Ver Fonte

chore: remove discord related modules and usage

gyroflaw há 1 ano atrás
pai
commit
24bd6d6f45
56 ficheiros alterados com 718 adições e 2430 exclusões
  1. 452 278
      package-lock.json
  2. 0 4
      package.json
  3. 3 26
      src/app.module.ts
  4. 1 2
      src/blockchain/event.emitter.ts
  5. 2 37
      src/forum/base.event.handler.ts
  6. 0 59
      src/forum/forum.embeds.ts
  7. 2 7
      src/forum/forum.module.ts
  8. 1 24
      src/forum/post-created.handler.ts
  9. 1 22
      src/forum/thread-created.handler.ts
  10. 17 23
      src/governance/base-event.handler.ts
  11. 4 11
      src/governance/governance.module.ts
  12. 19 16
      src/governance/proposal-created.handler.ts
  13. 14 16
      src/governance/proposal-decision-made.handler.ts
  14. 21 17
      src/governance/proposal-post-created.handler.ts
  15. 21 17
      src/governance/proposal-post-deleted.handler.ts
  16. 22 18
      src/governance/proposal-post-updated.handler.ts
  17. 0 268
      src/governance/proposals-embeds.ts
  18. 8 15
      src/governance/vote-cast.handler.ts
  19. 1 3
      src/gql/pioneer.client.ts
  20. 0 32
      src/identity/cacheable-members.provider.ts
  21. 0 125
      src/identity/claim.command.ts
  22. 0 9
      src/identity/claim.dto.ts
  23. 0 17
      src/identity/council.service.ts
  24. 0 34
      src/identity/identity.module.ts
  25. 0 30
      src/identity/pending-verification-cleaner.service.ts
  26. 0 292
      src/identity/rolesync.service.ts
  27. 0 135
      src/identity/solve.command.ts
  28. 0 6
      src/identity/solve.dto.ts
  29. 0 28
      src/storage-providers/endpoint.provider.ts
  30. 0 116
      src/storage-providers/sp-health-checker.service.ts
  31. 0 10
      src/storage-providers/sp.embeds.ts
  32. 0 19
      src/storage-providers/sp.module.ts
  33. 0 5
      src/types.ts
  34. 11 37
      src/util.ts
  35. 1 16
      src/videos/video-created.handler.ts
  36. 0 43
      src/videos/video.embeds.ts
  37. 2 7
      src/videos/videos.module.ts
  38. 15 24
      src/wg/application-created.handler.ts
  39. 7 20
      src/wg/application-withdrawn.handler.ts
  40. 6 24
      src/wg/base-event.handler.ts
  41. 3 13
      src/wg/budget-set.handler.ts
  42. 6 23
      src/wg/budget-spending.handler.ts
  43. 6 39
      src/wg/budget-updated.handler.ts
  44. 0 166
      src/wg/embeds.ts
  45. 5 19
      src/wg/lead.handler.ts
  46. 8 29
      src/wg/opening-added-or-cancelled.handler.ts
  47. 13 22
      src/wg/opening-filled.handler.ts
  48. 10 23
      src/wg/reward-paid.handler.ts
  49. 11 23
      src/wg/reward-updated.handler.ts
  50. 0 66
      src/wg/simulate.ts
  51. 6 21
      src/wg/stake.handler.ts
  52. 6 17
      src/wg/termination.handler.ts
  53. 1 6
      src/wg/wg.module.ts
  54. 2 43
      src/wg/wg.service.ts
  55. 2 15
      src/wg/worker-exited.handler.ts
  56. 8 13
      tsconfig.json

Diff do ficheiro suprimidas por serem muito extensas
+ 452 - 278
package-lock.json


+ 0 - 4
package.json

@@ -20,8 +20,6 @@
     "postinstall": "npm run generate"
   },
   "dependencies": {
-    "@discord-nestjs/common": "^3.4.2",
-    "@discord-nestjs/core": "^3.6.0",
     "@golevelup/nestjs-graphql-request": "^0.1.13",
     "@graphql-codegen/cli": "^2.13.1",
     "@graphql-codegen/typescript-graphql-request": "^4.5.5",
@@ -38,13 +36,11 @@
     "axios": "^0.27.2",
     "cache-manager": "^4.1.0",
     "crypto": "^1.0.1",
-    "discord.js": "^13.11.0",
     "graphql": "^16.6.0",
     "graphql-request": "^4.3.0",
     "install": "^0.13.0",
     "moment-duration-format": "^2.3.2",
     "nanoid": "^3.3.4",
-    "npm": "^8.19.2",
     "pg": "^8.8.0",
     "reflect-metadata": "0.1.13",
     "rimraf": "3.0.2",

+ 3 - 26
src/app.module.ts

@@ -1,16 +1,13 @@
 import { WgModule } from './wg/wg.module';
-import { IdentityModule } from './identity/identity.module';
-import { DiscordModule, DiscordModuleOption } from '@discord-nestjs/core';
+
 import { Module } from '@nestjs/common';
-import { ConfigModule, ConfigService } from '@nestjs/config';
-import { Intents, Message } from 'discord.js';
+import { ConfigModule } from '@nestjs/config';
 import { ScheduleModule } from '@nestjs/schedule';
 import { ForumModule } from './forum/forum.module';
 import { PioneerGraphQLModule } from './gql/pioneer.module';
 import { BlockchainModule } from './blockchain/blockchain.module';
 import { VideoModule } from './videos/videos.module';
 import { AtlasGraphQLModule } from './gql/atlas.module';
-import { StorageProvidersModule } from './storage-providers/sp.module';
 import { JoyGovernanceModule } from './governance/governance.module';
 
 @Module({
@@ -19,32 +16,12 @@ import { JoyGovernanceModule } from './governance/governance.module';
     ScheduleModule.forRoot(),
     PioneerGraphQLModule,
     AtlasGraphQLModule,
-    DiscordModule.forRootAsync({
-      imports: [ConfigModule],
-      useFactory: (configService: ConfigService) => ({
-        token: configService.get('TOKEN'),
-        discordClientOptions: {
-          intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MEMBERS],
-        },
-        registerCommandOptions: [
-          {
-            forGuild: configService.get('DISCORD_SERVER'),
-            allowFactory: (message: Message) =>
-              !message.author.bot && message.content === '!deploy',
-            removeCommandsBefore: true,
-          },
-        ],
-      } as DiscordModuleOption),
-      inject: [ConfigService],
-    }),
     BlockchainModule,
     WgModule,
-    IdentityModule,
     ForumModule,
     VideoModule,
-    StorageProvidersModule,
     JoyGovernanceModule,
   ],
   providers: [],
 })
-export class AppModule {}
+export class AppModule {}

+ 1 - 2
src/blockchain/event.emitter.ts

@@ -1,4 +1,3 @@
-import { Once } from '@discord-nestjs/core';
 import { Injectable, Logger } from '@nestjs/common';
 import { ConfigService } from '@nestjs/config';
 import { EventEmitter2 } from '@nestjs/event-emitter';
@@ -17,7 +16,7 @@ export class EventEmitterService {
     private readonly configService: ConfigService,
   ) {}
 
-  @Once('ready')
+  // TODO @Once('ready')
   async onReady(): Promise<void> {
     const wsLocation = this.configService.get<string>('RPC_URL');
     const api: ApiPromise = await connectApi(

+ 2 - 37
src/forum/base.event.handler.ts

@@ -1,45 +1,10 @@
-import { Client, TextChannel } from 'discord.js';
-import { InjectDiscordClient, Once } from '@discord-nestjs/core';
-import { Injectable, Logger, Optional } from '@nestjs/common';
+import { Injectable, Logger } from '@nestjs/common';
 
-import { forumCategoriesToChannels } from 'config';
 import { RetryablePioneerClient } from 'src/gql/pioneer.client';
-import { DiscordChannels } from 'src/types';
-import { getDiscordChannels } from 'src/util';
 
 @Injectable()
 export abstract class BaseEventHandler {
-  constructor(
-    protected readonly queryNodeClient: RetryablePioneerClient,
-    @InjectDiscordClient()
-    protected readonly client: Client,
-    @Optional()
-    protected channels: DiscordChannels,
-  ) {}
-
-  @Once('ready')
-  async onReady(): Promise<void> {
-    this.channels = await getDiscordChannels(this.client);
-  }
-
-  findChannelsByCategoryId(
-    categoryId: number,
-    parentCategoryId?: number,
-  ): TextChannel[] {
-    const mappedChannels = forumCategoriesToChannels.find(
-      (mapping) => mapping.category.id == categoryId,
-    )?.channels;
-    if (!mappedChannels) {
-      this.getLogger().log(
-        `Mapped channels not found for categoryId=${categoryId}, parentCategory=${parentCategoryId}`,
-      );
-      return [];
-    }
-    const oneD = [] as TextChannel[];
-    for (const row of Object.values(this.channels))
-      for (const e of row) oneD.push(e);
-    return oneD.filter((ch: TextChannel) => mappedChannels.includes(ch.name));
-  }
+  constructor(protected readonly queryNodeClient: RetryablePioneerClient) {}
 
   abstract getLogger(): Logger;
 }

+ 0 - 59
src/forum/forum.embeds.ts

@@ -1,59 +0,0 @@
-import Discord from 'discord.js';
-import { EventRecord } from '@polkadot/types/interfaces';
-import { joystreamBlue } from '../../config';
-import { ForumThreadByIdQuery, PostByIdQuery } from 'src/qntypes';
-
-export function getNewThreadEmbed(
-  thread: ForumThreadByIdQuery,
-  blockNumber: number,
-  event: EventRecord,
-): Discord.MessageEmbed {
-  const f = thread.forumThreadByUniqueInput;
-  return addCommonProperties(
-    new Discord.MessageEmbed()
-      .setTitle(
-        `New thread [${f?.title}] by user [${f?.author.handle}] created in [${f?.category.title}]`,
-      )
-      .setDescription(threadUrl(f?.id)),
-    blockNumber,
-    event,
-  );
-}
-
-export function getNewPostEmbed(
-  post: PostByIdQuery,
-  blockNumber: number,
-  event: EventRecord,
-): Discord.MessageEmbed {
-  const p = post.forumPostByUniqueInput;
-  return addCommonProperties(
-    new Discord.MessageEmbed()
-      .setTitle(
-        `New post by [${p?.author.handle}] created in [${p?.thread.title}]`,
-      )
-      .setDescription(postUrl(p?.thread.id, p?.id))
-      .addFields({
-        name: 'Category',
-        value: p?.thread.category.title || ' ',
-        inline: true,
-      }),
-    blockNumber,
-    event,
-  );
-}
-
-function addCommonProperties(
-  embed: Discord.MessageEmbed,
-  blockNumber: number,
-  event: EventRecord,
-) {
-  return embed.setColor(joystreamBlue).setTimestamp();
-}
-
-function threadUrl(threadId: string | undefined) {
-  return `https://pioneerapp.xyz//#/forum/thread/${threadId}`;
-}
-
-function postUrl(threadId: string | undefined, postId: string | undefined) {
-  return `${threadUrl(threadId)}?post=${postId}`;
-}

+ 2 - 7
src/forum/forum.module.ts

@@ -1,16 +1,11 @@
 import { Module } from '@nestjs/common';
-import { DiscordModule } from '@discord-nestjs/core';
 import { ConfigModule } from '@nestjs/config';
 import { PioneerGraphQLModule } from 'src/gql/pioneer.module';
 import { ThreadCreatedHandler } from './thread-created.handler';
 import { PostCreatedHandler } from './post-created.handler';
 
 @Module({
-  imports: [
-    DiscordModule.forFeature(), 
-    ConfigModule.forRoot(),
-    PioneerGraphQLModule
-  ],
+  imports: [ConfigModule.forRoot(), PioneerGraphQLModule],
   providers: [ThreadCreatedHandler, PostCreatedHandler],
 })
-export class ForumModule {}
+export class ForumModule {}

+ 1 - 24
src/forum/post-created.handler.ts

@@ -1,9 +1,7 @@
-import { TextChannel } from 'discord.js';
 import { Injectable, Logger } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
 import { ForumPostId } from '@joystream/types/primitives';
 
-import { getNewPostEmbed } from './forum.embeds';
 import { PostByIdQuery } from 'src/qntypes';
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base.event.handler';
@@ -17,28 +15,7 @@ export class PostCreatedHandler extends BaseEventHandler {
     const { data } = payload.event.event;
     const postId = data[0] as ForumPostId;
     const post = await this.queryNodeClient.postById(postId.toString());
-    const serverChannels = this.findChannelsByPost(post);
-    serverChannels.forEach((ch) => {
-      this.logger.debug(
-        `Sending to channel [${ch.id.toString()}] [${ch.name}]`,
-      );
-      ch.send({
-        embeds: [getNewPostEmbed(post, payload.block, payload.event)],
-      });
-    });
-  }
-
-  findChannelsByPost(post: PostByIdQuery): TextChannel[] {
-    if (!post.forumPostByUniqueInput) return [];
-
-    const { id, parentId } = post.forumPostByUniqueInput.thread.category;
-
-    this.logger.debug(id, parentId);
-
-    return this.findChannelsByCategoryId(
-      parseInt(id, 10),
-      parentId ? parseInt(parentId, 10) : undefined,
-    );
+    //  TODO
   }
 
   getLogger(): Logger {

+ 1 - 22
src/forum/thread-created.handler.ts

@@ -1,11 +1,8 @@
-import { TextChannel } from 'discord.js';
 import { Injectable, Logger } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
 import { ForumThreadId } from '@joystream/types/primitives';
 
-import { ForumThreadByIdQuery } from 'src/qntypes';
 import { EventWithBlock } from 'src/types';
-import { getNewThreadEmbed } from './forum.embeds';
 import { BaseEventHandler } from './base.event.handler';
 
 @Injectable()
@@ -19,25 +16,7 @@ export class ThreadCreatedHandler extends BaseEventHandler {
     const thread = await this.queryNodeClient.forumThreadById(
       threadId.toString(),
     );
-    const serverChannels = this.findChannelsByThread(thread);
-    serverChannels?.forEach((ch: TextChannel) => {
-      this.logger.debug(
-        `Sending to channel [${ch.id.toString()}] [${ch.name}]`,
-      );
-      ch.send({
-        embeds: [getNewThreadEmbed(thread, payload.block, payload.event)],
-      });
-    });
-  }
-
-  findChannelsByThread(thread: ForumThreadByIdQuery): TextChannel[] {
-    if (!thread.forumThreadByUniqueInput) return [];
-
-    const { id, parentId } = thread.forumThreadByUniqueInput.category;
-    return this.findChannelsByCategoryId(
-      parseInt(id, 10),
-      parentId ? parseInt(parentId, 10) : undefined,
-    );
+    // TODO
   }
 
   getLogger(): Logger {

+ 17 - 23
src/governance/base-event.handler.ts

@@ -1,22 +1,14 @@
-import { InjectDiscordClient } from '@discord-nestjs/core'
-import { Injectable } from '@nestjs/common'
-import { Client } from 'discord.js'
-import { RetryablePioneerClient } from 'src/gql/pioneer.client'
-import { CouncilService } from 'src/identity/council.service'
-import { AugmentedEvents, AugmentedEvent } from '@polkadot/api/types'
-import { EventRecord } from '@polkadot/types/interfaces/system'
+import { Injectable } from '@nestjs/common';
+import { RetryablePioneerClient } from 'src/gql/pioneer.client';
+import { AugmentedEvents, AugmentedEvent } from '@polkadot/api/types';
+import { EventRecord } from '@polkadot/types/interfaces/system';
 
 const PROPOSALS_CHANNEL_KEY = 'proposals';
-type ExtractTuple<P> = P extends AugmentedEvent<'rxjs', infer T> ? T : never
+type ExtractTuple<P> = P extends AugmentedEvent<'rxjs', infer T> ? T : never;
 
 @Injectable()
 export abstract class BaseEventHandler {
-  constructor(
-    protected readonly queryNodeClient: RetryablePioneerClient,
-    protected readonly councilService: CouncilService,
-    @InjectDiscordClient()
-    protected readonly client: Client) {
-  }
+  constructor(protected readonly queryNodeClient: RetryablePioneerClient) {}
 
   protected getProposalsChannelKey() {
     return PROPOSALS_CHANNEL_KEY;
@@ -24,10 +16,10 @@ export abstract class BaseEventHandler {
 
   protected async getMemberHandleById(id: string) {
     let member;
-    try{
+    try {
       member = await this.queryNodeClient.memberById(id);
       return member.memberships[0].handle;
-    } catch(e) {
+    } catch (e) {
       return id;
     }
   }
@@ -37,21 +29,23 @@ export abstract class BaseEventHandler {
     Module extends keyof AugmentedEvents<'rxjs'>,
     Event extends keyof AugmentedEvents<'rxjs'>[Module],
     Tuple extends ExtractTuple<AugmentedEvents<'rxjs'>[Module][Event]>,
-    Index extends keyof Tuple
+    Index extends keyof Tuple,
   >(
     events: EventRecord[],
     module: Module,
     eventName: Event,
-    index: Index = 0 as Index
+    index: Index = 0 as Index,
   ): Tuple[Index] | undefined {
-    const eventRecord = events.find((event) => event.event.method === eventName)
+    const eventRecord = events.find(
+      (event) => event.event.method === eventName,
+    );
 
     if (!eventRecord) {
-      return
+      return;
     }
 
-    const data = eventRecord.event.data as unknown as Tuple
+    const data = eventRecord.event.data as unknown as Tuple;
 
-    return data[index]
+    return data[index];
   }
-}
+}

+ 4 - 11
src/governance/governance.module.ts

@@ -1,9 +1,8 @@
-import { DiscordModule } from '@discord-nestjs/core';
 import { Module } from '@nestjs/common';
 import { ConfigModule } from '@nestjs/config';
 import { DatabaseModule } from 'src/db/database.module';
 import { PioneerGraphQLModule } from 'src/gql/pioneer.module';
-import { IdentityModule } from 'src/identity/identity.module';
+
 import { ProposalCreatedHandler } from './proposal-created.handler';
 import { ProposalDecisionMadeHandler } from './proposal-decision-made.handler';
 import { ProposalPostCreatedHandler } from './proposal-post-created.handler';
@@ -12,13 +11,7 @@ import { ProposalPostUpdatedHandler } from './proposal-post-updated.handler';
 import { ProposalVotedHandler } from './vote-cast.handler';
 
 @Module({
-  imports: [
-    DatabaseModule,
-    DiscordModule.forFeature(),
-    ConfigModule.forRoot(),
-    PioneerGraphQLModule,
-    IdentityModule
-  ],
+  imports: [DatabaseModule, ConfigModule.forRoot(), PioneerGraphQLModule],
   providers: [
     ProposalCreatedHandler,
     ProposalDecisionMadeHandler,
@@ -26,6 +19,6 @@ import { ProposalVotedHandler } from './vote-cast.handler';
     ProposalPostCreatedHandler,
     ProposalPostDeletedHandler,
     ProposalPostUpdatedHandler,
-  ]
+  ],
 })
-export class JoyGovernanceModule {}
+export class JoyGovernanceModule {}

+ 19 - 16
src/governance/proposal-created.handler.ts

@@ -1,30 +1,33 @@
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { channelNames } from 'config';
 import { EventWithBlock } from 'src/types';
-import { findDiscordChannel } from 'src/util';
 import { BaseEventHandler } from './base-event.handler';
-import { getProposalCreatedEmbed } from './proposals-embeds';
 
 @Injectable()
 export class ProposalCreatedHandler extends BaseEventHandler {
-
   @OnEvent('*.ProposalCreated')
   async handleProposalCreatedEvent(payload: EventWithBlock) {
-
     const [proposalId, generalInformation, proposalDetails] = [
-      this.getDataFromEvent([payload.event], 'proposalsCodex', 'ProposalCreated', 0),
-      this.getDataFromEvent([payload.event], 'proposalsCodex', 'ProposalCreated', 1),
-      this.getDataFromEvent([payload.event], 'proposalsCodex', 'ProposalCreated', 2)
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsCodex',
+        'ProposalCreated',
+        0,
+      ),
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsCodex',
+        'ProposalCreated',
+        1,
+      ),
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsCodex',
+        'ProposalCreated',
+        2,
+      ),
     ];
 
     const authorId = generalInformation?.memberId.toString() || '';
-
-    const channelToUse = findDiscordChannel(this.client, channelNames[this.getProposalsChannelKey()])[0];
-    channelToUse.send({
-      embeds: [
-        getProposalCreatedEmbed(proposalId, generalInformation, proposalDetails, await this.getMemberHandleById(authorId))
-      ],
-    });
   }
-}
+}

+ 14 - 16
src/governance/proposal-decision-made.handler.ts

@@ -1,27 +1,25 @@
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { channelNames } from 'config';
+
 import { EventWithBlock } from 'src/types';
-import { findDiscordChannel } from 'src/util';
 import { BaseEventHandler } from './base-event.handler';
-import { getProposalDecidedEmbed } from './proposals-embeds';
-
 @Injectable()
 export class ProposalDecisionMadeHandler extends BaseEventHandler {
-
   @OnEvent('proposalsEngine.ProposalDecisionMade')
   async handleProposalDecisionMadeEvent(payload: EventWithBlock) {
-
     const [proposalId, decision] = [
-      this.getDataFromEvent([payload.event], 'proposalsEngine', 'ProposalDecisionMade', 0),
-      this.getDataFromEvent([payload.event], 'proposalsEngine', 'ProposalDecisionMade', 1)
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsEngine',
+        'ProposalDecisionMade',
+        0,
+      ),
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsEngine',
+        'ProposalDecisionMade',
+        1,
+      ),
     ];
-
-    const channelToUse = findDiscordChannel(this.client, channelNames[this.getProposalsChannelKey()])[0];
-    channelToUse.send({
-      embeds: [
-        getProposalDecidedEmbed(proposalId, decision?.toString() || 'Status Unknown')
-      ],
-    });
   }
-}
+}

+ 21 - 17
src/governance/proposal-post-created.handler.ts

@@ -1,31 +1,35 @@
-
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { channelNames } from 'config';
 import { EventWithBlock } from 'src/types';
-import { findDiscordChannel } from 'src/util';
 import { BaseEventHandler } from './base-event.handler';
-import { getPostCreatedEmbed } from './proposals-embeds';
 
 @Injectable()
 export class ProposalPostCreatedHandler extends BaseEventHandler {
-
   @OnEvent('proposalsDiscussion.PostCreated')
   async handleProposalPostCreatedEvent(payload: EventWithBlock) {
-
     const [memberId, proposalId, content] = [
-      this.getDataFromEvent([payload.event], 'proposalsDiscussion', 'PostCreated', 1),
-      this.getDataFromEvent([payload.event], 'proposalsDiscussion', 'PostCreated', 2),
-      this.getDataFromEvent([payload.event], 'proposalsDiscussion', 'PostCreated', 3)
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsDiscussion',
+        'PostCreated',
+        1,
+      ),
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsDiscussion',
+        'PostCreated',
+        2,
+      ),
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsDiscussion',
+        'PostCreated',
+        3,
+      ),
     ];
 
-    const memberHandle = await this.getMemberHandleById(memberId?.toString() || '');
-
-    const channelToUse = findDiscordChannel(this.client, channelNames[this.getProposalsChannelKey()])[0];
-    channelToUse.send({
-      embeds: [
-        getPostCreatedEmbed(proposalId, memberHandle, content?.toString() || 'Empty Body')
-      ],
-    });
+    const memberHandle = await this.getMemberHandleById(
+      memberId?.toString() || '',
+    );
   }
 }

+ 21 - 17
src/governance/proposal-post-deleted.handler.ts

@@ -1,32 +1,36 @@
-
 import { createType } from '@joystream/types';
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { channelNames } from 'config';
 import { EventWithBlock } from 'src/types';
-import { findDiscordChannel } from 'src/util';
 import { BaseEventHandler } from './base-event.handler';
-import { getPostDeletedEmbed } from './proposals-embeds';
 
 @Injectable()
 export class ProposalPostDeletedHandler extends BaseEventHandler {
-
   @OnEvent('proposalsDiscussion.PostDeleted')
   async handleProposalPostDeletedEvent(payload: EventWithBlock) {
-
     const [memberId, proposalId, postId] = [
-      this.getDataFromEvent([payload.event], 'proposalsDiscussion', 'PostDeleted', 0),
-      this.getDataFromEvent([payload.event], 'proposalsDiscussion', 'PostDeleted', 1),
-      this.getDataFromEvent([payload.event], 'proposalsDiscussion', 'PostDeleted', 2)
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsDiscussion',
+        'PostDeleted',
+        0,
+      ),
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsDiscussion',
+        'PostDeleted',
+        1,
+      ),
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsDiscussion',
+        'PostDeleted',
+        2,
+      ),
     ];
 
-    const memberHandle = await this.getMemberHandleById(memberId?.toString() || '');
-
-    const channelToUse = findDiscordChannel(this.client, channelNames[this.getProposalsChannelKey()])[0];
-    channelToUse.send({
-      embeds: [
-        getPostDeletedEmbed(proposalId, memberHandle, postId || createType('u64', 0))
-      ],
-    });
+    const memberHandle = await this.getMemberHandleById(
+      memberId?.toString() || '',
+    );
   }
 }

+ 22 - 18
src/governance/proposal-post-updated.handler.ts

@@ -1,32 +1,36 @@
-
-import { createType } from '@joystream/types';
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { channelNames } from 'config';
+
 import { EventWithBlock } from 'src/types';
-import { findDiscordChannel } from 'src/util';
 import { BaseEventHandler } from './base-event.handler';
-import { getPostUpdatedEmbed } from './proposals-embeds';
 
 @Injectable()
 export class ProposalPostUpdatedHandler extends BaseEventHandler {
-
   @OnEvent('proposalsDiscussion.PostUpdated')
   async handleProposalPostUpdatedEvent(payload: EventWithBlock) {
-
     const [postId, memberId, proposalId] = [
-      this.getDataFromEvent([payload.event], 'proposalsDiscussion', 'PostUpdated', 0),
-      this.getDataFromEvent([payload.event], 'proposalsDiscussion', 'PostUpdated', 1),
-      this.getDataFromEvent([payload.event], 'proposalsDiscussion', 'PostUpdated', 2)
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsDiscussion',
+        'PostUpdated',
+        0,
+      ),
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsDiscussion',
+        'PostUpdated',
+        1,
+      ),
+      this.getDataFromEvent(
+        [payload.event],
+        'proposalsDiscussion',
+        'PostUpdated',
+        2,
+      ),
     ];
 
-    const memberHandle = await this.getMemberHandleById(memberId?.toString() || '');
-
-    const channelToUse = findDiscordChannel(this.client, channelNames[this.getProposalsChannelKey()])[0];
-    channelToUse.send({
-      embeds: [
-        getPostUpdatedEmbed(proposalId, memberHandle, postId || createType('u64', 0))
-      ],
-    });
+    const memberHandle = await this.getMemberHandleById(
+      memberId?.toString() || '',
+    );
   }
 }

+ 0 - 268
src/governance/proposals-embeds.ts

@@ -1,268 +0,0 @@
-import {
-  ProposalId,
-  ProposalDiscussionPostId,
-  ProposalDiscussionThreadId,
-} from '@joystream/types/primitives';
-import { Vec } from '@polkadot/types';
-import { ITuple } from '@polkadot/types/types';
-import { Balance } from '@polkadot/types/interfaces/runtime';
-import {
-  PalletCommonBalanceKind,
-  PalletProposalsCodexGeneralProposalParams,
-} from '@polkadot/types/lookup';
-import { PalletProposalsCodexProposalDetails } from '@polkadot/types/lookup';
-import { PalletCommonFundingRequestParameters } from '@polkadot/types/lookup';
-import { PalletCommonWorkingGroupIterableEnumsWorkingGroup } from '@polkadot/types/lookup';
-import Discord from 'discord.js';
-import { joystreamBlue } from '../../config';
-import { hexToString, formatBalance } from '@polkadot/util';
-
-formatBalance.setDefaults({
-  decimals: 10, //TODO clarify
-  unit: 'JOY',
-});
-
-export function getProposalCreatedEmbed(
-  proposalId: ProposalId | undefined,
-  generalInformation: PalletProposalsCodexGeneralProposalParams | undefined,
-  proposalDetails: PalletProposalsCodexProposalDetails | undefined,
-  authorHandleOrId: string,
-): Discord.MessageEmbed {
-  if (proposalDetails?.isSignal) {
-    return getSignalProposalCreatedEmbed(
-      proposalId,
-      generalInformation,
-      proposalDetails.asSignal.toString(),
-      authorHandleOrId,
-    );
-  } else if (proposalDetails?.isFundingRequest) {
-    return getFundingProposalCreatedEmbed(
-      proposalId,
-      generalInformation,
-      proposalDetails.asFundingRequest,
-      authorHandleOrId,
-    );
-  } else if (proposalDetails?.isUpdateWorkingGroupBudget) {
-    return getWgBudgetProposalCreatedEmbed(
-      proposalId,
-      generalInformation,
-      proposalDetails.asUpdateWorkingGroupBudget,
-      authorHandleOrId,
-    );
-  } else {
-    return new Discord.MessageEmbed()
-      .setTitle(
-        `🏛 New Proposal '${
-          hexToString(generalInformation?.title.toString()) || ''
-        }' Created`,
-      )
-      .addFields([
-        {
-          name: 'Link',
-          value: proposalUrl(proposalId?.toString() || ''),
-          inline: true,
-        },
-        { name: 'Author', value: authorHandleOrId, inline: true },
-      ])
-      .setColor(joystreamBlue)
-      .setTimestamp();
-  }
-}
-
-function getSignalProposalCreatedEmbed(
-  proposalId: ProposalId | undefined,
-  generalInformation: PalletProposalsCodexGeneralProposalParams | undefined,
-  signal: string,
-  authorHandleOrId: string,
-): Discord.MessageEmbed {
-  return new Discord.MessageEmbed()
-    .setTitle(
-      `🏛 Signal Proposal '${
-        hexToString(generalInformation?.title.toString()) || ''
-      }' Created`,
-    )
-    .setDescription(hexToString(signal))
-    .addFields([
-      {
-        name: 'Link',
-        value: proposalUrl(proposalId?.toString() || ''),
-        inline: true,
-      },
-      { name: 'Author', value: authorHandleOrId, inline: true },
-    ])
-    .setColor(joystreamBlue)
-    .setTimestamp();
-}
-
-function getFundingProposalCreatedEmbed(
-  proposalId: ProposalId | undefined,
-  generalInformation: PalletProposalsCodexGeneralProposalParams | undefined,
-  fundingData: Vec<PalletCommonFundingRequestParameters>,
-  authorHandleOrId: string,
-): Discord.MessageEmbed {
-  const fundingInfo = fundingData
-    .map(
-      (funding) =>
-        `${formatBalance(
-          funding.amount.toString(),
-        )} ➡️ ${funding.account.toString()}`,
-    )
-    .join('\n');
-  return new Discord.MessageEmbed()
-    .setTitle(
-      `🏛 Funding Proposal '${
-        hexToString(generalInformation?.title.toString()) || ''
-      }' Created`,
-    )
-    .setDescription(
-      hexToString(generalInformation?.description.toString()) || 'N/A',
-    )
-    .addFields([
-      {
-        name: 'Link',
-        value: proposalUrl(proposalId?.toString() || ''),
-        inline: true,
-      },
-      { name: 'Author', value: authorHandleOrId, inline: true },
-      { name: 'Funding', value: fundingInfo, inline: true },
-    ])
-    .setColor(joystreamBlue)
-    .setTimestamp();
-}
-
-function getWgBudgetProposalCreatedEmbed(
-  proposalId: ProposalId | undefined,
-  generalInformation: PalletProposalsCodexGeneralProposalParams | undefined,
-  budgetData: ITuple<
-    [
-      Balance,
-      PalletCommonWorkingGroupIterableEnumsWorkingGroup,
-      PalletCommonBalanceKind,
-    ]
-  >,
-  authorHandleOrId: string,
-): Discord.MessageEmbed {
-  return new Discord.MessageEmbed()
-    .setTitle(
-      `${
-        budgetData[2].isNegative ? 'DECREASE' : 'Update'
-      } ${budgetData[1].toString()} budget by ${formatBalance(
-        budgetData[0].toString(),
-      )}`,
-    )
-    .addFields([
-      {
-        name: 'Link',
-        value: proposalUrl(proposalId?.toString() || ''),
-        inline: true,
-      },
-      { name: 'Author', value: authorHandleOrId, inline: true },
-    ])
-    .setColor(joystreamBlue)
-    .setTimestamp();
-}
-
-export function getProposalDecidedEmbed(
-  proposalId: ProposalId | undefined,
-  decision: string,
-): Discord.MessageEmbed {
-  return new Discord.MessageEmbed()
-    .setTitle(`Proposal ${proposalId?.toString()} ${decision}`)
-    .addFields([
-      {
-        name: 'Link',
-        value: proposalUrl(proposalId?.toString() || ''),
-        inline: true,
-      },
-    ])
-    .setColor(joystreamBlue)
-    .setTimestamp();
-}
-
-export function getVotedEmbed(
-  proposalId: ProposalId | undefined,
-  voter: string,
-  vote: string,
-  rationale: string,
-): Discord.MessageEmbed {
-  return new Discord.MessageEmbed()
-    .setTitle(`Proposal ${proposalId?.toString()} got ${vote} vote`)
-    .setDescription(hexToString(rationale))
-    .addFields([
-      {
-        name: 'Link',
-        value: proposalUrl(proposalId?.toString() || ''),
-        inline: true,
-      },
-      { name: 'Voter CM', value: voter, inline: true },
-    ])
-    .setColor(joystreamBlue)
-    .setTimestamp();
-}
-
-// why is threadId is the same as proposalId? Awkward convention...
-export function getPostCreatedEmbed(
-  proposalId: ProposalDiscussionThreadId | undefined,
-  member: string,
-  postText: string,
-): Discord.MessageEmbed {
-  return new Discord.MessageEmbed()
-    .setTitle(`New post under the proposal ${proposalId?.toString()}`)
-    .setDescription(hexToString(postText))
-    .addFields([
-      {
-        name: 'Proposal URL',
-        value: proposalUrl(proposalId?.toString() || ''),
-        inline: true,
-      },
-      { name: 'Created By', value: member, inline: true },
-    ])
-    .setColor(joystreamBlue)
-    .setTimestamp();
-}
-
-export function getPostDeletedEmbed(
-  proposalId: ProposalDiscussionThreadId | undefined,
-  member: string,
-  postId: ProposalDiscussionPostId,
-): Discord.MessageEmbed {
-  return new Discord.MessageEmbed()
-    .setTitle(
-      `Deleted post ${postId.toString()} under the proposal ${proposalId?.toString()}`,
-    )
-    .addFields([
-      {
-        name: 'Proposal URL',
-        value: proposalUrl(proposalId?.toString() || ''),
-        inline: true,
-      },
-      { name: 'Deleted By', value: member, inline: true },
-    ])
-    .setColor(joystreamBlue)
-    .setTimestamp();
-}
-
-export function getPostUpdatedEmbed(
-  proposalId: ProposalDiscussionThreadId | undefined,
-  member: string,
-  postId: ProposalDiscussionPostId,
-): Discord.MessageEmbed {
-  return new Discord.MessageEmbed()
-    .setTitle(
-      `Updated post ${postId.toString()} under the proposal ${proposalId?.toString()}`,
-    )
-    .addFields([
-      {
-        name: 'Proposal URL',
-        value: proposalUrl(proposalId?.toString() || ''),
-        inline: true,
-      },
-      { name: 'Updated By', value: member, inline: true },
-    ])
-    .setColor(joystreamBlue)
-    .setTimestamp();
-}
-
-function proposalUrl(id: string) {
-  return `https://pioneerapp.xyz//#/proposals/preview/${id}`;
-}

+ 8 - 15
src/governance/vote-cast.handler.ts

@@ -1,32 +1,25 @@
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { channelNames } from 'config';
+
 import { EventWithBlock } from 'src/types';
-import { findDiscordChannel } from 'src/util';
 import { BaseEventHandler } from './base-event.handler';
-import { getVotedEmbed } from './proposals-embeds';
 
 @Injectable()
 export class ProposalVotedHandler extends BaseEventHandler {
-
   @OnEvent('proposalsEngine.Voted')
   async handleProposalVotedEvent(payload: EventWithBlock) {
-
     const [voter, proposal, vote, rationale] = [
       this.getDataFromEvent([payload.event], 'proposalsEngine', 'Voted', 0),
       this.getDataFromEvent([payload.event], 'proposalsEngine', 'Voted', 1),
       this.getDataFromEvent([payload.event], 'proposalsEngine', 'Voted', 2),
-      this.getDataFromEvent([payload.event], 'proposalsEngine', 'Voted', 3)
+      this.getDataFromEvent([payload.event], 'proposalsEngine', 'Voted', 3),
     ];
 
     const voterHandle = await this.getMemberHandleById(voter?.toString() || '');
-    const voteEmoji = vote?.isApprove ? '👍🏻' : vote?.isReject ? '👎' : 'abstain';
-
-    const channelToUse = findDiscordChannel(this.client, channelNames[this.getProposalsChannelKey()])[0];
-    channelToUse.send({
-      embeds: [
-        getVotedEmbed(proposal, voterHandle, voteEmoji, rationale?.toString() || 'No Rationale')
-      ],
-    });
+    const voteEmoji = vote?.isApprove
+      ? '👍🏻'
+      : vote?.isReject
+      ? '👎'
+      : 'abstain';
   }
-}
+}

+ 1 - 3
src/gql/pioneer.client.ts

@@ -1,4 +1,3 @@
-import { UseFilters } from '@discord-nestjs/core';
 import { Inject, Injectable, Logger } from '@nestjs/common';
 import { globalRetryConfig } from 'config';
 import {
@@ -7,13 +6,12 @@ import {
   Sdk,
 } from 'src/qntypes';
 import { Retryable } from 'typescript-retry-decorator';
-import { PioneerException, PioneerExceptionFilter } from './pioeer.exception';
+import { PioneerException } from './pioeer.exception';
 
 /**
  * Decorator around the autogenerated `Sdk` that enables retry mechanism for Query Node calls
  */
 @Injectable()
-@UseFilters(PioneerExceptionFilter)
 export class RetryablePioneerClient {
   private readonly logger = new Logger(RetryablePioneerClient.name);
 

+ 0 - 32
src/identity/cacheable-members.provider.ts

@@ -1,32 +0,0 @@
-import { CACHE_MANAGER, Inject, Injectable, Logger } from '@nestjs/common';
-import { RetryablePioneerClient } from 'src/gql/pioneer.client';
-import { Cache } from 'cache-manager';
-import { MembersByHandlesQuery } from 'src/qntypes';
-import {createHash as hash} from 'crypto';
-
-@Injectable()
-export class CacheableMembershipsProvider {
-  private readonly logger = new Logger(CacheableMembershipsProvider.name);
-
-  constructor(
-    protected readonly pioneerClient: RetryablePioneerClient,
-    @Inject(CACHE_MANAGER) private cacheManager: Cache
-  ) { }
-
-  async getMembers(handles: string[]): Promise<MembersByHandlesQuery> {
-    const cacheKey = this.buildCacheKey(handles);
-    const cached: MembersByHandlesQuery | undefined = await this.cacheManager.get(cacheKey);
-    if (cached) { // hit 
-      return cached;
-    } else { // miss
-      this.logger.debug('Cache miss');
-      const qnResult = await this.pioneerClient.membersByHandles(handles);
-      this.cacheManager.set(cacheKey, qnResult);
-      return qnResult;
-    }
-  }
-  
-  buildCacheKey(handles: string[]): string {
-    return hash('sha1').update(handles.join(',')).digest('hex');
-  }
-}

+ 0 - 125
src/identity/claim.command.ts

@@ -1,125 +0,0 @@
-import { TransformPipe } from '@discord-nestjs/common';
-import {
-  Command,
-  DiscordTransformedCommand,
-  Payload,
-  TransformedCommandExecutionContext,
-  UsePipes,
-} from '@discord-nestjs/core';
-import { Inject, Logger } from '@nestjs/common';
-import {
-  CacheType,
-  CommandInteraction,
-  ContextMenuInteraction,
-} from 'discord.js';
-import { PendingVerification } from 'src/db/pending-verification.entity';
-import { ClaimDto } from './claim.dto';
-import { nanoid } from 'nanoid';
-import { MemberByHandleQuery, Sdk } from 'src/qntypes';
-
-@Command({
-  name: 'claim',
-  description: 'Claim an on-chain identity',
-})
-@UsePipes(TransformPipe)
-export class IdentityClaimCommand
-  implements DiscordTransformedCommand<ClaimDto>
-{
-  private readonly logger = new Logger(IdentityClaimCommand.name);
-
-  constructor(
-    @Inject('PENDING_VERIFICATION_REPOSITORY')
-    private readonly pendingVerificationRepository: typeof PendingVerification,
-    @Inject('PioneerGqlSdk') private readonly pioneerApi: Sdk,
-  ) {}
-
-  async handler(
-    @Payload() dto: ClaimDto,
-    context: TransformedCommandExecutionContext,
-  ) {
-    this.logger.log(
-      `${this.buildHandle(context.interaction)} claiming on-chain identity '${
-        dto.username
-      }' (${dto.wallet})`,
-    );
-
-    // verify that the address really belongs to the claimed membership
-    let queryNodeMember: MemberByHandleQuery | null = null;
-    try {
-      // Note: retryable client isn't used here. Why? Discord command only has 2 seconds to respond to user. Retries take longer :(
-      queryNodeMember = await this.pioneerApi.memberByHandle({
-        handle: dto.username,
-      });
-    } catch (error) {
-      this.logger.warn(`Username ${dto.username} not found`);
-    }
-
-    if (
-      queryNodeMember &&
-      queryNodeMember.memberships.length > 0 &&
-      (queryNodeMember.memberships[0].controllerAccount === dto.wallet ||
-        queryNodeMember.memberships[0].rootAccount === dto.wallet)
-    ) {
-      this.logger.log(
-        `${this.buildHandle(context.interaction)} claiming on-chain identity '${
-          dto.username
-        }' (${dto.wallet})`,
-      );
-      // existing pending verification check
-      // TODO how to make sure only one pending verification exist for a given user?
-      const verification = await this.pendingVerificationRepository.findOne({
-        where: {
-          startedByDiscordHandle: this.buildHandle(context.interaction),
-        },
-        raw: true,
-      });
-      if (verification) {
-        context.interaction.reply({
-          content: `You already started to claim on-chain identity. Use \`/solve\` command to finish the process.\nChallenge: ${verification.challenge}`,
-          ephemeral: true,
-        });
-        return;
-      } else {
-        const challenge = nanoid();
-        const created = await this.pendingVerificationRepository.create({
-          claimedMembership: dto.username,
-          claimedAccountAddress: dto.wallet,
-          startedByDiscordHandle: this.buildHandle(context.interaction),
-          challenge: challenge,
-        });
-        if (created) {
-          this.logger.log(
-            `${this.buildHandle(
-              context.interaction,
-            )} initiated claiming on-chain identity '${dto.username}' (${
-              dto.wallet
-            })`,
-          );
-          context.interaction.reply({
-            content: `Copy the following string and sign it using [Polkadot App](https://polkadot.js.org/apps/?rpc=wss://rpc.joystream.org:9944/#/signing):\n\`${challenge}\`\nThen, use \`/solve\` command to finish the process.`,
-            ephemeral: true,
-          });
-          return;
-        } else {
-          context.interaction.reply({
-            content: `Well, this is embarassing, but I have to ask you to try again later.`,
-            ephemeral: true,
-          });
-          return;
-        }
-      }
-    } else {
-      context.interaction.reply({
-        content: `You cannot claim this identity`,
-        ephemeral: true,
-      });
-    }
-  }
-  buildHandle(
-    interaction:
-      | CommandInteraction<CacheType>
-      | ContextMenuInteraction<CacheType>,
-  ): string {
-    return `${interaction.user.username}#${interaction.user.discriminator}`;
-  }
-}

+ 0 - 9
src/identity/claim.dto.ts

@@ -1,9 +0,0 @@
-import { Param } from '@discord-nestjs/core';
-
-
-export class ClaimDto {
-  @Param({ description: 'Your Joystream username', required: true})
-    username!: string;
-  @Param({ description: 'Root or controller address of your Joystream user', required: true})
-    wallet!: string;
-}

+ 0 - 17
src/identity/council.service.ts

@@ -1,17 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { RetryablePioneerClient } from 'src/gql/pioneer.client';
-
-/**
- * Public service that other modules may want to use to get Council information
- */
-@Injectable()
-export class CouncilService {
-
-  constructor(
-    private readonly queryNodeClient: RetryablePioneerClient,
-  ) { }
-
-  async fetchCurrentCouncilMembers() {
-    return await this.queryNodeClient.activeCouncilMembers();
-  }
-}

+ 0 - 34
src/identity/identity.module.ts

@@ -1,34 +0,0 @@
-
-import { CacheModule, Module } from '@nestjs/common';
-import { DatabaseModule } from '../db/database.module';
-import { DiscordModule } from '@discord-nestjs/core';
-import { IdentityClaimCommand } from './claim.command';
-import { SolveChallengeCommand } from './solve.command';
-import { ConfigModule } from '@nestjs/config';
-import { PendingVerificationCleaner } from './pending-verification-cleaner.service';
-import { RoleSyncService } from './rolesync.service';
-import { CouncilService } from './council.service';
-import { PioneerGraphQLModule } from 'src/gql/pioneer.module';
-import { CacheableMembershipsProvider } from './cacheable-members.provider';
-
-@Module({
-  imports: [
-    DatabaseModule,
-    DiscordModule.forFeature(),
-    ConfigModule.forRoot(),
-    PioneerGraphQLModule,
-    CacheModule.register({ttl: 60*60}), // cached for 1h
-  ], 
-  providers: [
-    IdentityClaimCommand, 
-    SolveChallengeCommand, 
-    PendingVerificationCleaner,
-    CacheableMembershipsProvider,
-    RoleSyncService,
-    CouncilService
-  ],
-  exports: [
-    CouncilService
-  ]
-})
-export class IdentityModule {}

+ 0 - 30
src/identity/pending-verification-cleaner.service.ts

@@ -1,30 +0,0 @@
-import { Inject, Injectable, Logger } from '@nestjs/common';
-import { Cron, CronExpression } from '@nestjs/schedule';
-import { PendingVerification } from 'src/db/pending-verification.entity';
-import moment from 'moment';
-import { Op } from 'sequelize';
-
-@Injectable()
-export class PendingVerificationCleaner {
-  private readonly logger = new Logger(PendingVerificationCleaner.name);
-
-  constructor(
-    @Inject('PENDING_VERIFICATION_REPOSITORY')
-    private readonly pendingVerificationRepository: typeof PendingVerification
-  ) { }
-
-  @Cron(CronExpression.EVERY_30_MINUTES)
-  async cleanupOldPendingVerifications(): Promise<void> {
-    this.logger.debug('Cleaning old pending verifications...');
-    const deletedRecords = await this.pendingVerificationRepository.destroy({
-      where: {
-        createdAt: {
-          [Op.lte]: moment().subtract(30, 'minutes')
-        }
-      }
-    });
-    if (deletedRecords) {
-      this.logger.debug(`${deletedRecords} records cleaned`);
-    }
-  }
-}

+ 0 - 292
src/identity/rolesync.service.ts

@@ -1,292 +0,0 @@
-import { Inject, Injectable, Logger } from '@nestjs/common';
-import { Cron, CronExpression } from '@nestjs/schedule';
-import { DaoMembership } from 'src/db/dao-membership.entity';
-import { DaoRole } from 'src/db/dao-role.entity';
-import { Op } from 'sequelize';
-import { wgToRoleMap } from '../../config';
-import { ConfigService } from '@nestjs/config';
-import { InjectDiscordClient } from '@discord-nestjs/core';
-import { Client, GuildMember, Role } from 'discord.js';
-import { findServerRole } from 'src/util';
-import { RetryablePioneerClient } from 'src/gql/pioneer.client';
-import { MemberByHandleQuery } from 'src/qntypes';
-import { CacheableMembershipsProvider } from './cacheable-members.provider';
-
-const CM_ROLE = 'councilMemberRole';
-const FM_ROLE = 'foundingMemberRole';
-
-type Unpacked<T> = T extends (infer U)[] ? U : T;
-type Membership = Unpacked<MemberByHandleQuery['memberships']>;
-type OnChainRole = Unpacked<Membership['roles']>;
-
-/**
- * Cron-based syncing of Joystream on-chain roles with Discord server roles.
- * On-chain roles are fetched from Query node for all Discord users who claimed their Joystream memberships.
- */
-@Injectable()
-export class RoleSyncService {
-  private readonly logger = new Logger(RoleSyncService.name);
-
-  constructor(
-    @Inject('DAO_MEMBERSHIP_REPOSITORY')
-    private readonly daoMembershipRepository: typeof DaoMembership,
-    @Inject('DAO_ROLE_REPOSITORY')
-    private readonly daoRoleRepository: typeof DaoRole,
-    @InjectDiscordClient()
-    private readonly client: Client,
-    private readonly configService: ConfigService,
-    private readonly queryNodeClient: RetryablePioneerClient,
-    private readonly membershipsProvider: CacheableMembershipsProvider,
-  ) {}
-
-  @Cron(CronExpression.EVERY_30_MINUTES)
-  async syncOnChainRoles() {
-    this.logger.debug('Syncing on-chain roles');
-    const activeCouncilMembers =
-      await this.queryNodeClient.activeCouncilMembers();
-    const totalVerifiedMembersCount =
-      await this.daoMembershipRepository.count();
-    let page = 0;
-    const pageSize = 50;
-    while (page * pageSize < totalVerifiedMembersCount) {
-      const memberships = await this.getPageOfMemberships(pageSize, page);
-
-      const memberHandles: string[] = memberships.map((m) => m.membership);
-      for (let i = 0; i < memberships.length; i++) {
-        const ithMember = memberships[i];
-        const mainServer = this.configService.get('DISCORD_SERVER');
-        const serverUser = await this.findUser(mainServer, ithMember);
-        // next 'if' block checks whether a user exists in the server and cleans the role data if they left (or changed the nickname)
-        if (!serverUser) {
-          this.logger.warn(
-            `User ${ithMember.discordHandle} not found on this server. Cleaning the data`,
-          );
-          this.daoRoleRepository.destroy({
-            where: {
-              membershipId: ithMember.id,
-            },
-          });
-          this.daoMembershipRepository.destroy({
-            where: {
-              id: ithMember.id,
-            },
-          });
-          continue;
-        }
-
-        // Bulk Query Node call to get the on-chain roles
-        const queryNodeMember = await this.findMembership(
-          memberHandles,
-          ithMember,
-        );
-        if (!queryNodeMember) {
-          continue;
-        }
-
-        // Keep only active roles, filter the others out
-        const onChainRoles = queryNodeMember.roles.filter(
-          (role) => role.status.__typename === 'WorkerStatusActive',
-        );
-
-        // first pass: assigning server roles based on joystream ones
-        for (let r = 0; r < onChainRoles.length; r++) {
-          const isLead = onChainRoles[r].isLead;
-          const roleInJoystream =
-            onChainRoles[r].groupId + (isLead ? 'Lead' : '');
-
-          await this.maybeAssignRole(roleInJoystream, ithMember, serverUser);
-        }
-
-        // second pass: revokation of server roles that user doesn't have anymore in joystream
-        for (let m = 0; m < ithMember.daoRoles.length; m++) {
-          const dbRole = ithMember.daoRoles[m];
-          if (dbRole.role === CM_ROLE || dbRole.role === FM_ROLE) continue; // CM & FM roles are handled separately
-          await this.maybeRevokeRole(dbRole, ithMember, onChainRoles);
-        }
-
-        // assign founding member role if needed
-        const { isFoundingMember } = queryNodeMember;
-        if (isFoundingMember) {
-          await this.maybeAssignRole(FM_ROLE, ithMember, serverUser);
-        } else {
-          const fmRole = ithMember.daoRoles.find(
-            (role) => role.role === FM_ROLE,
-          );
-          // Revoke FM role if user was assigned before
-          if (fmRole) {
-            await this.revokeRole(fmRole, ithMember, onChainRoles);
-          }
-        }
-
-        // assign council member role if needed
-        const isUserInCouncilCurrently =
-          activeCouncilMembers.electedCouncils[0].councilMembers.find(
-            (cm: any) => cm.member.handle === ithMember.membership,
-          ) !== undefined;
-        if (isUserInCouncilCurrently) {
-          await this.maybeAssignRole(CM_ROLE, ithMember, serverUser);
-        }
-
-        // revoke council member role if needed
-        const cmRole = ithMember.daoRoles.find((role) => role.role === CM_ROLE);
-        if (cmRole && !queryNodeMember.isCouncilMember) {
-          await this.revokeRole(cmRole, ithMember, onChainRoles);
-        }
-      }
-      page = page + 1;
-    }
-  }
-
-  private async findMembership(memberHandles: string[], member: DaoMembership) {
-    let queryNodeMember: MemberByHandleQuery | null = null;
-    try {
-      queryNodeMember = await this.membershipsProvider.getMembers(
-        memberHandles,
-      );
-      return queryNodeMember.memberships.find(
-        (mm) => mm.handle === member.membership,
-      );
-    } catch (error) {
-      this.logger.warn(`Member ${member.membership} doesn't exist`);
-      return null;
-    }
-  }
-
-  private async maybeAssignRole(
-    roleInJoystream: string,
-    ithMember: DaoMembership,
-    serverUser: GuildMember,
-  ) {
-    // Check that user's on-chain role is already stored in our database.
-    // If it's not, user needs to be granted this role, and new DaoRole record created for this user.
-    if (!this.hasDbRole(ithMember, roleInJoystream)) {
-      const mainServer = this.configService.get('DISCORD_SERVER');
-      const roleToAssign = (await findServerRole(
-        this.client,
-        mainServer,
-        wgToRoleMap[roleInJoystream],
-      )) as Role;
-
-      if (roleToAssign) {
-        await serverUser.roles.add(
-          roleToAssign.id,
-          'Assigned as per on-chain role',
-        );
-        this.daoRoleRepository.create({
-          role: roleInJoystream,
-          membershipId: ithMember.id,
-        });
-        this.logger.debug(
-          `Assigned ${ithMember.discordHandle} server role [${wgToRoleMap[roleInJoystream]}]`,
-        );
-      } else {
-        this.logger.warn(
-          `I was about to assign role ${wgToRoleMap[roleInJoystream]}, but it's gone!`,
-        );
-      }
-    }
-  }
-
-  private async maybeRevokeRole(
-    dbRole: DaoRole,
-    ithMember: DaoMembership,
-    onChainRoles: OnChainRole[],
-  ) {
-    // Check that user's db role is still relevant.
-    // If it's not, user needs to be revoked this role, and corresponding DaoRole record deleted for this user.
-    if (!this.hasOnChainRole(onChainRoles, dbRole.role)) {
-      this.revokeRole(dbRole, ithMember, onChainRoles);
-    }
-  }
-
-  private async revokeRole(
-    dbRole: DaoRole,
-    ithMember: DaoMembership,
-    onChainRoles: OnChainRole[],
-  ) {
-    const mainServer = this.configService.get('DISCORD_SERVER');
-    const roleToRevoke = (await findServerRole(
-      this.client,
-      mainServer,
-      wgToRoleMap[dbRole.role],
-    )) as Role;
-
-    if (roleToRevoke) {
-      const serverUser = await this.findUser(mainServer, ithMember);
-      if (serverUser) {
-        await serverUser.roles.remove(
-          roleToRevoke.id,
-          'Revoked as per on-chain changes',
-        );
-        this.daoRoleRepository.destroy({
-          where: {
-            id: dbRole.id,
-          },
-        });
-        this.logger.debug(
-          `Revoked ${ithMember.discordHandle} server role [${
-            wgToRoleMap[dbRole.role]
-          }]`,
-        );
-      } else {
-        this.logger.warn(
-          `User ${ithMember.discordHandle} not found on this server`,
-        );
-      }
-    } else {
-      this.logger.warn(
-        `I was about to revoke role [${
-          wgToRoleMap[dbRole.role]
-        }], but it's gone!`,
-      );
-    }
-  }
-
-  private async findUser(mainServerId: string, membership: DaoMembership) {
-    const server = await this.client.guilds.fetch(mainServerId);
-    const usernameParts = membership.discordHandle.split('#');
-    const serverUsers = await server.members.fetch({ query: usernameParts[0] });
-    const serverUser = serverUsers.find(
-      (mem) => mem.user.discriminator === usernameParts[1],
-    );
-    return serverUser;
-  }
-
-  private hasDbRole(member: DaoMembership, onChainRole: string): boolean {
-    return member.daoRoles.find((r) => r.role === onChainRole) !== undefined;
-  }
-
-  private hasOnChainRole(onChainRoles: OnChainRole[], dbRole: string): boolean {
-    return onChainRoles.find((r) => dbRole.includes(r.groupId)) !== undefined;
-  }
-
-  private async getPageOfMemberships(
-    pageSize: number,
-    page: number,
-  ): Promise<DaoMembership[]> {
-    // first query only selects ids from the master table (memberships)
-    const fetchIds = await this.daoMembershipRepository.findAll({
-      limit: pageSize,
-      offset: page * pageSize,
-      attributes: ['id'],
-    });
-
-    // second query selects memberships + relevant roles in one go (using outer join)
-    // note the absence of limit/offset in this query!
-    const pageOfMemberships = await this.daoMembershipRepository.findAll({
-      where: {
-        id: {
-          [Op.in]: fetchIds.map<number>((record: DaoMembership) => record.id),
-        },
-      },
-      include: DaoRole,
-    });
-
-    this.logger.debug(
-      `Fetched ${pageOfMemberships.length} records ${pageOfMemberships.map(
-        (dao) => dao.id,
-      )}`,
-    );
-    return pageOfMemberships;
-  }
-}

+ 0 - 135
src/identity/solve.command.ts

@@ -1,135 +0,0 @@
-import { TransformPipe } from '@discord-nestjs/common';
-import {
-  Command,
-  DiscordTransformedCommand,
-  InjectDiscordClient,
-  Payload,
-  TransformedCommandExecutionContext,
-  UsePipes,
-} from '@discord-nestjs/core';
-import { Inject, Logger } from '@nestjs/common';
-import { CacheType, Client, CommandInteraction, ContextMenuInteraction, Role } from 'discord.js';
-import { PendingVerification } from 'src/db/pending-verification.entity';
-import { DaoMembership } from 'src/db/dao-membership.entity';
-import { SolveDto } from './solve.dto';
-import { signatureVerify } from '@polkadot/util-crypto';
-import { findServerRole } from 'src/util';
-import { ConfigService } from '@nestjs/config';
-import { identityValidatedRole } from 'config';
-
-@Command({
-  name: 'solve',
-  description: 'Finish claiming an on-chain identity',
-})
-@UsePipes(TransformPipe)
-export class SolveChallengeCommand implements DiscordTransformedCommand<SolveDto> {
-  private readonly logger = new Logger(SolveChallengeCommand.name);
-
-  constructor(
-    @Inject('PENDING_VERIFICATION_REPOSITORY')
-    private readonly pendingVerificationRepository: typeof PendingVerification,
-    @Inject('DAO_MEMBERSHIP_REPOSITORY')
-    private readonly daoMembershipRepository: typeof DaoMembership,
-    @InjectDiscordClient()
-    private readonly client: Client,
-    private readonly configService: ConfigService 
-  ) { }
-
-  async handler(@Payload() dto: SolveDto, context: TransformedCommandExecutionContext) {
-
-    // length check
-    if (dto.challenge?.length !== 130) {
-      this.reply(`Signed challenge must be exactly 130 symbols. You sent a string with ${dto.challenge.length} symbols`, context);
-      return;
-    }
-    // existing pending verification check
-    // TODO how to make sure only one pending verification exist for a given user? 
-    const verification = await this.pendingVerificationRepository.findOne(
-      {
-        where: {
-          startedByDiscordHandle: this.buildHandle(context.interaction)
-        },
-        raw: true
-      });
-
-    if (verification) {
-      this.logger.debug(`Verifying that challenge '${verification.challenge}' signature '${dto.challenge}' was signed by address '${verification.claimedAccountAddress}'`);
-      try {
-        const { isValid } = signatureVerify(verification.challenge, dto.challenge, verification.claimedAccountAddress);
-        if (!isValid) {
-          this.reply(`Signed challenge you provided is not correct`, context);
-          return;
-        }
-      } catch (error) {
-        this.logger.debug(`Verification failed for '${verification.claimedAccountAddress}'`);
-        this.reply(`Signed challenge you provided is not correct`, context);
-        return;
-      }
-      // verify that this address isn't yet claimed
-      const existingBinding = await this.daoMembershipRepository.findOne(
-        {
-          where: {
-            membership: verification.claimedMembership
-          },
-          raw: true
-        });
-
-      if (existingBinding) {
-        this.logger.log(`Identity '${existingBinding.membership}' already claimed by '${existingBinding.discordHandle}'`);
-        this.reply(`🤔 This identity seems to be already claimed`, context);
-        return
-      } else {
-        const created = await this.daoMembershipRepository.create(
-          {
-            membership: verification.claimedMembership,
-            accountAddress: verification.claimedAccountAddress,
-            discordHandle: this.buildHandle(context.interaction)
-          });
-        if (created) {
-          this.logger.log(`${this.buildHandle(context.interaction)} claimed identity '${verification.claimedMembership}'`);
-          // clean up the pending verification records
-          await this.pendingVerificationRepository.destroy(
-            {
-              where: {
-                startedByDiscordHandle: this.buildHandle(context.interaction)
-              }
-            }
-          );
-
-          // assign 'identity verified' server role 
-          const serverToCheck = this.configService.get('DISCORD_SERVER');
-          const verifiedRole = await findServerRole(
-            this.client, 
-            serverToCheck, 
-            identityValidatedRole) as Role;
-          this.logger.debug(verifiedRole);
-          const serverUser = await context.interaction.guild?.members.fetch({user: context.interaction.user});
-          
-          await serverUser?.roles.add(verifiedRole.id, 'Verified via claiming on-chain identity');
-          
-          this.reply(`Congrats! You have successfully claimed the identity. Your on-chain roles should show up within 30 minutes`, context);
-          return
-        } else {
-          this.logger.log(`Creating record failed.`);
-          this.reply(`Well, this is embarassing, but I have to ask you to try again later.`, context);
-          return
-        }
-      }
-    } else {
-      this.logger.log(`No pending verification for user ${this.buildHandle(context.interaction)}`);
-      this.reply(`You don't have any pending verifications. Claim your Joystream account using \`/claim\` command`, context);
-      return
-    }
-  }
-
-  buildHandle(interaction: CommandInteraction<CacheType> | ContextMenuInteraction<CacheType>): string {
-    return `${interaction.user.username}#${interaction.user.discriminator}`;
-  }
-
-  reply(replyText: string, context: TransformedCommandExecutionContext) {
-    context.interaction.reply({
-      content: replyText,
-      ephemeral: true
-    });    
-  }
-}

+ 0 - 6
src/identity/solve.dto.ts

@@ -1,6 +0,0 @@
-import { Param } from '@discord-nestjs/core';
-
-export class SolveDto {
-  @Param({ description: 'Signed challenge', required: true})
-    challenge!: string;
-}

+ 0 - 28
src/storage-providers/endpoint.provider.ts

@@ -1,28 +0,0 @@
-import { CACHE_MANAGER, Inject, Injectable, Logger } from '@nestjs/common';
-import { RetryablePioneerClient } from 'src/gql/pioneer.client';
-import { Cache } from 'cache-manager';
-
-const CACHE_KEY = 'sp-nodes-key';
-
-@Injectable()
-export class StorageNodeEndpointProvider {
-  private readonly logger = new Logger(StorageNodeEndpointProvider.name);
-
-  constructor(
-    protected readonly pioneerClient: RetryablePioneerClient,
-    @Inject(CACHE_MANAGER) private cacheManager: Cache
-  ) { }
-
-  async getStorageNodeEndpoints(): Promise<string[]> {
-    const cached: string[] | undefined = await this.cacheManager.get(CACHE_KEY);
-    if (cached) { // hit 
-      return cached; 
-    } else { // miss
-      this.logger.debug('Loading storage node data from QN...');
-      const qnResult = await this.pioneerClient.getStorageNodes();
-      const endpoints = qnResult.storageBuckets.map((n) => n.operatorMetadata?.nodeEndpoint as string);
-      this.cacheManager.set(CACHE_KEY, endpoints);
-      return endpoints;
-    }
-  }
-}

+ 0 - 116
src/storage-providers/sp-health-checker.service.ts

@@ -1,116 +0,0 @@
-import { InjectDiscordClient } from '@discord-nestjs/core';
-import { Inject, Injectable, Logger } from '@nestjs/common';
-import { Client } from 'discord.js';
-import { Cron, CronExpression } from '@nestjs/schedule';
-import { StorageNodeEndpointProvider } from './endpoint.provider';
-import { axiosConfig, channelNames, wgToRoleMap } from '../../config';
-import { UnhealthyStorageProvider } from 'src/db/unhealthy-storage.entity';
-import { findDiscordChannel, findServerRole } from 'src/util';
-import { getFaultyNodesEmbed } from './sp.embeds';
-import axios from 'axios';
-import { ConfigService } from '@nestjs/config';
-import { Op } from 'sequelize';
-import { RetryablePioneerClient } from 'src/gql/pioneer.client';
-import { GetStorageBagsByNodeEndpointQuery } from 'src/qntypes';
-
-const SP_CHANNEL_KEY = 'storageWorkingGroup';
-
-@Injectable()
-export class StorageProviderHealthChecker {
-  private readonly logger = new Logger(StorageProviderHealthChecker.name);
-
-  constructor(
-    protected readonly pioneerClient: RetryablePioneerClient,
-    protected readonly endpointProvider: StorageNodeEndpointProvider,
-    @InjectDiscordClient()
-    protected readonly client: Client,
-    @Inject('UNHEALTHY_STORAGE_PROVIDER_REPOSITORY')
-    private readonly unhealthyStorageProviderRepository: typeof UnhealthyStorageProvider,
-    private readonly configService: ConfigService
-  ) { }
-
-  @Cron(CronExpression.EVERY_10_MINUTES)
-  async reportFaultyNodes() {
-    const badNodes = await this.unhealthyStorageProviderRepository.findAll();
-    let badNodesSummaryList = '';
-    badNodes.forEach(async (badNode: UnhealthyStorageProvider, index: number) => {
-      const bags = await this.pioneerClient.getStorageBagsByNodeEndpoint(badNode.endpoint);
-      if(!this.allBagsHaveSingleBucket(bags)) {
-        badNodesSummaryList = badNodesSummaryList.concat(`${index + 1}. ${badNode.endpoint}\n`);
-      }
-    });
-
-    if (badNodesSummaryList !== '') {
-      const serverToCheck = this.configService.get('DISCORD_SERVER');
-      const storageProvidersWorkingGroupRole = await findServerRole(this.client, serverToCheck, wgToRoleMap[SP_CHANNEL_KEY]);
-      const channelToUse = findDiscordChannel(this.client, channelNames[SP_CHANNEL_KEY])[0];
-      channelToUse.send({
-        embeds: [
-          getFaultyNodesEmbed(badNodesSummaryList, storageProvidersWorkingGroupRole)
-        ],
-      });
-      this.logger.warn(`Reported ${badNodes.length} bad nodes`);
-    }
-  }
-
-  @Cron(CronExpression.EVERY_30_SECONDS)
-  async registerFaultyNodes() {
-    let endpoints = await this.endpointProvider.getStorageNodeEndpoints();
-    let healthyNodes: string[] = [];
-    await Promise.all(
-      endpoints.map(async (endpoint: string) => {
-        try {
-          const pingResponse = await axios.get(`${endpoint}api/v1/state/data`, axiosConfig);
-          if (pingResponse.status !== 200) {
-            this.logger.warn(`Node ${endpoint} health check failed. HTTP status: ${pingResponse.status}`);
-            const bags = await this.pioneerClient.getStorageBagsByNodeEndpoint(endpoint);
-            if(!this.allBagsHaveSingleBucket(bags)) {
-              await this.storeFaultyNode(endpoint);
-            }
-          } else {
-            healthyNodes.push(endpoint);
-          }
-        } catch (e) {
-          this.logger.warn(`Node ${endpoint} health check failed`, e);
-          await this.storeFaultyNode(endpoint);
-        }
-      })
-    );
-    healthyNodes = healthyNodes.length ? healthyNodes : ['bogus endpoint'];
-    endpoints = endpoints.length ? endpoints : ['bogus endpoint'];
-    await this.unhealthyStorageProviderRepository.destroy(
-      {
-        where: {
-          [Op.or]: [
-          // optimization: bulk delete nodes that became healthy
-            {
-              endpoint: {
-                [Op.in]: healthyNodes
-              }
-            },
-          // remove failing nodes that don't show up in QN response anymore 
-          // (corner case discussed https://discord.com/channels/811216481340751934/812344681786507274/998970342301249556)
-            {
-              endpoint: {
-                [Op.notIn]: endpoints
-              }
-            }
-          ]
-        }
-      });
-  }
-
-  allBagsHaveSingleBucket(bags: GetStorageBagsByNodeEndpointQuery): boolean {
-    // this.logger.debug(bags);
-    return bags.storageBuckets.find( sb => sb.bags.find(bag => bag.storageBuckets.length !== 1)) === undefined; 
-  }
-
-  async storeFaultyNode(endpoint: string) {
-    const exists = await this.unhealthyStorageProviderRepository.count({ where: { endpoint: endpoint } });
-    if (exists === 0) {
-      this.unhealthyStorageProviderRepository.create({ endpoint: endpoint });
-    } else {
-      this.logger.debug(`Node ${endpoint} already registered as faulty`);
-    }
-  }
-}

+ 0 - 10
src/storage-providers/sp.embeds.ts

@@ -1,10 +0,0 @@
-import Discord, { Role } from 'discord.js';
-import { joystreamBlue } from '../../config'
-
-export function getFaultyNodesEmbed(summary: string, serverRoleToMention: Role | undefined): Discord.MessageEmbed {
-  return new Discord.MessageEmbed()
-    .setTitle(`🚨🚨🚨 Failing storage node(s) alert!`)
-    .setDescription(`${serverRoleToMention ? `<@&${serverRoleToMention.id}>` : ''} \n${summary}`)
-    .setColor(joystreamBlue)
-    .setTimestamp();
-}

+ 0 - 19
src/storage-providers/sp.module.ts

@@ -1,19 +0,0 @@
-import { CacheModule, Module } from '@nestjs/common';
-import { DiscordModule } from '@discord-nestjs/core';
-import { ConfigModule } from '@nestjs/config';
-import { PioneerGraphQLModule } from 'src/gql/pioneer.module';
-import { DatabaseModule } from 'src/db/database.module';
-import { StorageNodeEndpointProvider } from './endpoint.provider';
-import { StorageProviderHealthChecker } from './sp-health-checker.service';
-
-@Module({
-  imports: [
-    CacheModule.register({ttl: 60*60}), // cached for 1h
-    DiscordModule.forFeature(),
-    ConfigModule.forRoot(),
-    DatabaseModule,
-    PioneerGraphQLModule
-  ],
-  providers: [StorageNodeEndpointProvider, StorageProviderHealthChecker],
-})
-export class StorageProvidersModule {}

+ 0 - 5
src/types.ts

@@ -1,4 +1,3 @@
-import Discord from 'discord.js';
 import { EventRecord } from '@polkadot/types/interfaces';
 
 export interface ChannelNames {
@@ -9,10 +8,6 @@ export interface Licenses {
   [key: string]: string;
 }
 
-export interface DiscordChannels {
-  [key: string]: Discord.TextChannel[];
-}
-
 export interface EventWithBlock {
   block: number;
   event: EventRecord;

+ 11 - 37
src/util.ts

@@ -1,61 +1,35 @@
-import { AnyChannel, Client, Role, TextChannel } from 'discord.js';
-import { channelNames } from '../config';
 import { ApiPromise, WsProvider } from '@polkadot/api';
 import { Hash, EventRecord } from '@polkadot/types/interfaces';
 import { BlockNumber } from '@polkadot/types/interfaces';
 import { Vec } from '@polkadot/types';
-import { DiscordChannels } from './types';
 
 export async function connectApi(url: string): Promise<ApiPromise> {
   const provider = new WsProvider(url);
   return await ApiPromise.create({ provider });
 }
 
-export function getBlockHash(api: ApiPromise,  block: BlockNumber | number): Promise<Hash> {
+export function getBlockHash(
+  api: ApiPromise,
+  block: BlockNumber | number,
+): Promise<Hash> {
   try {
     return api.rpc.chain.getBlockHash(block);
   } catch (e) {
     return getBestHash(api);
   }
-};
+}
 
 export function getBestHash(api: ApiPromise) {
   return api.rpc.chain.getFinalizedHead();
 }
 
-export function getEvents(api: ApiPromise, hash: Hash): Promise<Vec<EventRecord>> {
+export function getEvents(
+  api: ApiPromise,
+  hash: Hash,
+): Promise<Vec<EventRecord>> {
   return api.query.system.events.at(hash);
-} 
-
-export async function getDiscordChannels (client: Client): Promise<DiscordChannels> {
-  const discordChannels: DiscordChannels = {};
-  Object.keys(channelNames).map(async (c) => {
-    const channel = findDiscordChannel(client, channelNames[c]);
-    if (channel && channel.length > 0) discordChannels[c] = channel;
-    else {
-      console.warn(`Channel '${channelNames[c]}' not found on this server`);
-    }
-  });
-  return discordChannels;
-};
-
-export function findDiscordChannel(client: Client,  name: string): TextChannel[] {
-  return client.channels.cache.filter(
-    (channel: any) => channel.name === name
-  ).map((value: AnyChannel) => value as TextChannel);
 }
 
-export async function findServerRole(
-  client: Client,
-  serverName: string,
-  roleName: string
-): Promise<Role | undefined> {
-  
-  const server = await client.guilds.fetch(serverName);
-  const role = server.roles.cache.find(role => role.name === roleName);
-  return role;
-}
-  
 export function delay(milliseconds: number) {
-  return new Promise( resolve => setTimeout(resolve, milliseconds) );
-}
+  return new Promise((resolve) => setTimeout(resolve, milliseconds));
+}

+ 1 - 16
src/videos/video-created.handler.ts

@@ -1,5 +1,3 @@
-import { Client } from 'discord.js';
-import { InjectDiscordClient, Once } from '@discord-nestjs/core';
 import { Injectable, Logger, Optional } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
 import { VideoId } from '@joystream/types/primitives';
@@ -10,8 +8,6 @@ import {
   GetVideoByIdQuery,
 } from 'src/qntypes-atlas';
 import { EventWithBlock } from 'src/types';
-import { findDiscordChannel } from 'src/util';
-import { getVideoEmbed } from './video.embeds';
 import { channelNames } from '../../config';
 
 const VIDEOS_CHANNEL_KEY = 'videos';
@@ -22,13 +18,11 @@ export class VideoCreatedHandler {
 
   constructor(
     protected readonly atlasClient: RetryableAtlasClient,
-    @InjectDiscordClient()
-    protected readonly client: Client,
     @Optional()
     protected distributionBuckets: GetDistributionBucketsWithOperatorsQuery,
   ) {}
 
-  @Once('ready')
+  // TODO @Once('ready')
   async onReady(): Promise<void> {
     this.distributionBuckets =
       await this.atlasClient.getDistributionBucketsWithOperators();
@@ -53,15 +47,6 @@ export class VideoCreatedHandler {
     this.logger.debug(videoQueryNodeResponse.videoByUniqueInput?.title);
     const bag = videoQueryNodeResponse.videoByUniqueInput?.media?.storageBag.id;
     const cdnUrl = this.getDistributorUrl(bag || ' ');
-    if (cdnUrl) {
-      const channelToUse = findDiscordChannel(
-        this.client,
-        channelNames[VIDEOS_CHANNEL_KEY],
-      )[0];
-      channelToUse.send({
-        embeds: [getVideoEmbed(videoQueryNodeResponse, cdnUrl)],
-      });
-    }
   }
 
   getDistributorUrl(bagId: string) {

+ 0 - 43
src/videos/video.embeds.ts

@@ -1,43 +0,0 @@
-import { joystreamBlue, licenses } from 'config';
-import { GetVideoByIdQuery } from 'src/qntypes-atlas';
-import Discord, { EmbedAuthorData } from 'discord.js';
-import { humanFileSize } from './sizeformat';
-import moment from 'moment';
-import 'moment-duration-format';
-
-export function getVideoEmbed(video: GetVideoByIdQuery, cdnUrl: string): Discord.MessageEmbed {
-  const vid = video.videoByUniqueInput;
-  const licenseKey = vid?.license?.code || ' ';
-  const exampleEmbed = new Discord.MessageEmbed()
-    .setColor(joystreamBlue)
-    .setTitle(vid?.title || ' ')
-    .setURL(`https://play.joystream.org/video/${vid?.id}`)
-    .setDescription(vid?.description?.substring(0, 200) || ' ') // cut off lengthy descriptions 
-    .addFields([
-      { name: 'ID', value: vid?.id || ' ', inline: true },
-      { name: 'Category', value: vid?.category?.name || ' ', inline: true },
-      { name: 'Duration', value: durationFormat(vid?.duration || 0), inline: true },
-      { name: 'Language', value: vid?.language?.iso || ' ', inline: true },
-      { name: 'Size', value: humanFileSize(vid?.media?.size), inline: true },
-      { name: 'License', value: licenses[licenseKey] || ' ', inline: true },
-    ]).setTimestamp();
-  const uploaderTitle = `${vid?.channel.title} (${vid?.channel.ownerMember?.controllerAccount})`
-  const avatarObj = vid?.channel.avatarPhoto?.id;
-  const author = {
-    name: uploaderTitle, 
-    iconURL: avatarObj ? `${cdnUrl}/${avatarObj}` : null, 
-    url: `https://play.joystream.org/channel/${vid?.channel?.id}`
-  } as EmbedAuthorData;
-  exampleEmbed.setAuthor(author);
-  console.log(`${cdnUrl}/${vid?.thumbnailPhoto?.id}`);
-  exampleEmbed.setImage(`${cdnUrl}/${vid?.thumbnailPhoto?.id}`);
-  return exampleEmbed;
-}
-
-function durationFormat(duration: number) {
-  if (duration < 60) {
-    return `${duration}s.`
-  } else {
-     return moment.duration(duration, 'seconds').format('hh:mm:ss')
-  }
-}

+ 2 - 7
src/videos/videos.module.ts

@@ -1,15 +1,10 @@
 import { Module } from '@nestjs/common';
-import { DiscordModule } from '@discord-nestjs/core';
 import { ConfigModule } from '@nestjs/config';
 import { VideoCreatedHandler } from './video-created.handler';
 import { AtlasGraphQLModule } from 'src/gql/atlas.module';
 
 @Module({
-  imports: [
-    DiscordModule.forFeature(),
-    ConfigModule.forRoot(),
-    AtlasGraphQLModule
-  ],
+  imports: [ConfigModule.forRoot(), AtlasGraphQLModule],
   providers: [VideoCreatedHandler],
 })
-export class VideoModule {}
+export class VideoModule {}

+ 15 - 24
src/wg/application-created.handler.ts

@@ -1,37 +1,28 @@
 import { ApplicationId } from '@joystream/types/primitives';
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
+import { PalletWorkingGroupApplyOnOpeningParams } from '@polkadot/types/lookup';
+
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getAppliedOnOpeningEmbed } from './embeds';
-import { PalletWorkingGroupApplyOnOpeningParams } from '@polkadot/types/lookup'
 
 @Injectable()
 export class ApplicationCreatedHandler extends BaseEventHandler {
-
   @OnEvent('*.AppliedOnOpening')
   async handleApplicationCreatedEvent(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
-    const applicationOpeningId = (data[0] as PalletWorkingGroupApplyOnOpeningParams).openingId;
-    const applicantId = (data[0] as PalletWorkingGroupApplyOnOpeningParams).memberId;
+
+    const applicationOpeningId = (
+      data[0] as PalletWorkingGroupApplyOnOpeningParams
+    ).openingId;
+    const applicantId = (data[0] as PalletWorkingGroupApplyOnOpeningParams)
+      .memberId;
     const applicationId = data[1] as ApplicationId;
-    const openingObject = await this.queryNodeClient.openingById(`${section}-${applicationOpeningId.toString()}`);
-    const applicant = await this.queryNodeClient.memberById(applicantId.toString());
-    this.channels[section].forEach((ch: TextChannel) =>
-      ch.send({
-        embeds: [
-          getAppliedOnOpeningEmbed(
-            applicationId,
-            openingObject,
-            applicant,
-            payload.block,
-            payload.event
-          ),
-        ],
-      }));
+    const openingObject = await this.queryNodeClient.openingById(
+      `${section}-${applicationOpeningId.toString()}`,
+    );
+    const applicant = await this.queryNodeClient.memberById(
+      applicantId.toString(),
+    );
   }
-}
+}

+ 7 - 20
src/wg/application-withdrawn.handler.ts

@@ -1,36 +1,23 @@
 import { ApplicationId } from '@joystream/types/primitives';
 import { Injectable, Logger } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
+
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getApplicationWithdrawnEmbed } from './embeds';
 
 @Injectable()
-export class ApplicationWithdrawnHandler extends BaseEventHandler{
+export class ApplicationWithdrawnHandler extends BaseEventHandler {
   private readonly logger = new Logger(ApplicationWithdrawnHandler.name);
 
   @OnEvent('*.ApplicationWithdrawn')
   async handleApplicationWithdrawnEvent(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if(!this.checkChannel(section)) {
-      return;
-    }
+
     const withdrawnId = data[0] as ApplicationId;
     const withdrawnApplicationKey = `${section}-${withdrawnId.toString()}`;
     this.logger.debug(withdrawnApplicationKey);
-    const withdrawnApplication = await this.queryNodeClient.applicationById(withdrawnApplicationKey);
-
-    this.channels[section].forEach((ch: TextChannel) =>
-      ch.send({
-        embeds: [
-          getApplicationWithdrawnEmbed(
-            withdrawnId,
-            withdrawnApplication,
-            payload.block,
-            payload.event
-          ),
-        ],
-      }));
+    const withdrawnApplication = await this.queryNodeClient.applicationById(
+      withdrawnApplicationKey,
+    );
   }
-}
+}

+ 6 - 24
src/wg/base-event.handler.ts

@@ -1,31 +1,13 @@
-import { InjectDiscordClient, Once } from '@discord-nestjs/core';
-import { Injectable, Optional } from '@nestjs/common';
-import { Client } from 'discord.js';
-import { DiscordChannels } from 'src/types';
-import { getDiscordChannels } from 'src/util';
+import { Injectable } from '@nestjs/common';
+
 import { RetryablePioneerClient } from 'src/gql/pioneer.client';
 
 @Injectable()
 export abstract class BaseEventHandler {
+  constructor(protected readonly queryNodeClient: RetryablePioneerClient) {}
 
-  constructor(
-    protected readonly queryNodeClient: RetryablePioneerClient,
-    @InjectDiscordClient()
-    protected readonly client: Client,
-    @Optional()
-    protected channels: DiscordChannels) {
-  }
-
-  @Once('ready')
+  // @Once('ready')
   async onReady(): Promise<void> {
-    this.channels = await getDiscordChannels(this.client);
-  }
-
-  protected checkChannel(section: string): boolean {
-    if (!this.channels[section] && section !== 'joystreamUtility') {
-      console.log(`Channel not configured for [${section}]`);
-      return false;
-    }
-    return true;
+    // TODO
   }
-}
+}

+ 3 - 13
src/wg/budget-set.handler.ts

@@ -1,26 +1,16 @@
 import { Balance } from '@polkadot/types/interfaces';
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
+
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getBudgetSetEmbed } from './embeds';
 
 @Injectable()
 export class BudgetSetHandler extends BaseEventHandler {
-
   @OnEvent('*.BudgetSet')
   async handleBudgetSetEvent(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
+
     const balance = (data[0] as Balance).toNumber();
-    this.channels[section].forEach((ch: TextChannel) =>
-      ch.send({
-        embeds: [
-          getBudgetSetEmbed(balance, payload.block, payload.event),
-        ],
-      }));
   }
-}
+}

+ 6 - 23
src/wg/budget-spending.handler.ts

@@ -1,37 +1,20 @@
 import { Balance } from '@polkadot/types/interfaces';
-import { Injectable} from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
+
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getDiscretionarySpendingEmbed, getDiscretionarySpendingToNonWorkerAddressEmbed } from './embeds';
 
 @Injectable()
 export class BudgetSpendingHandler extends BaseEventHandler {
-
   @OnEvent('*.BudgetSpending')
   async handleBudgetSpendingEvent(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
+
     const payee = data[0].toString();
-    const spendingAmount = data[1] as Balance
+    const spendingAmount = data[1] as Balance;
     try {
       const payeeWorker = await this.queryNodeClient.workersByAccount(payee);
-      this.channels[section].forEach((ch: TextChannel) =>
-        ch.send({
-          embeds: [
-            getDiscretionarySpendingEmbed(spendingAmount, payeeWorker, payload.block, payload.event),
-          ],
-        }));
-    } catch(e) {
-      this.channels[section].forEach((ch: TextChannel) =>
-        ch.send({
-          embeds: [
-            getDiscretionarySpendingToNonWorkerAddressEmbed(spendingAmount, payee, payload.block, payload.event),
-          ],
-        }));      
-    }
+    } catch (e) {}
   }
-}
+}

+ 6 - 39
src/wg/budget-updated.handler.ts

@@ -1,55 +1,22 @@
 import { Balance } from '@polkadot/types/interfaces';
 import { Injectable, Logger } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getBudgetSetEmbed } from './embeds';
+
 import { PalletCommonWorkingGroupIterableEnumsWorkingGroup } from '@polkadot/types/lookup';
 
 @Injectable()
 export class BudgetUpdatedHandler extends BaseEventHandler {
-    private readonly logger = new Logger(BudgetUpdatedHandler.name);
+  private readonly logger = new Logger(BudgetUpdatedHandler.name);
 
   @OnEvent('*.UpdatedWorkingGroupBudget')
   async handleBudgetUpdatedEvent(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
+
     const budgetChange = (data[1] as Balance).toNumber();
-    const wg: PalletCommonWorkingGroupIterableEnumsWorkingGroup = data[0] as PalletCommonWorkingGroupIterableEnumsWorkingGroup;
+    const wg: PalletCommonWorkingGroupIterableEnumsWorkingGroup =
+      data[0] as PalletCommonWorkingGroupIterableEnumsWorkingGroup;
     console.log(wg.toHuman());
-    let dynamicChannels: TextChannel[] = [];
-
-    if (wg.isForum) {
-      dynamicChannels = this.channels['forumWorkingGroup'];
-    } else if (wg.isContent) {
-      dynamicChannels = this.channels['contentWorkingGroup'];
-    } else if (wg.isOperationsAlpha) {
-      dynamicChannels = this.channels['operationsWorkingGroupAlpha'];
-    } else if (wg.isMembership) {
-      dynamicChannels = this.channels['membershipWorkingGroup'];
-    } else if (wg.isOperationsBeta) {
-      dynamicChannels = this.channels['operationsWorkingGroupBeta'];
-    } else if (wg.isOperationsGamma) {
-      dynamicChannels = this.channels['operationsWorkingGroupGamma'];
-    } else if (wg.isStorage) {
-      dynamicChannels = this.channels['storageWorkingGroup'];
-    } else if (wg.isDistribution) {
-      dynamicChannels = this.channels['distributionWorkingGroup'];
-    } else if (wg.isGateway) {
-      dynamicChannels = this.channels['gatewayWorkingGroup'];
-    }
-    if (!dynamicChannels || dynamicChannels.length == 0) {
-      this.logger.warn(`Channel not configured for [${section}]`);
-    } else {
-      dynamicChannels.forEach((ch: TextChannel) =>
-        ch.send({
-          embeds: [
-            getBudgetSetEmbed(budgetChange, payload.block, payload.event),
-          ],
-        }));
-    }
   }
-}
+}

+ 0 - 166
src/wg/embeds.ts

@@ -1,166 +0,0 @@
-import { joystreamBlue } from '../../config'
-import { formatBalance } from '@polkadot/util';
-import { EventRecord } from '@polkadot/types/interfaces';
-import Discord from 'discord.js';
-import { OpeningId, ApplicationId } from '@joystream/types/primitives';
-import { Balance } from '@polkadot/types/interfaces';
-import { OpeningByIdQuery, WorkerByIdQuery, ApplicationByIdQuery, MemberByIdQuery, WorkersByAccountQuery } from '../qntypes';
-import { EventWithBlock } from 'src/types';
-
-formatBalance.setDefaults({
-  decimals: 10, //TODO clarify
-  unit: 'JOY',
-})
-
-export function getBudgetSetEmbed(balanceSet: number, blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`💰 💵 💸 💴 💶 ${formatBalance(balanceSet)} added to the Treasury 💰 💵 💸 💴 💶 `)
-    , blockNumber, event);
-}
-
-export function getOpeningAddedEmbed(id: OpeningId, opening: OpeningByIdQuery, blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-  const openingData = opening.workingGroupOpeningByUniqueInput;
-  const description = openingData?.metadata.description ?
-    openingData.metadata.description : openingData?.metadata.shortDescription;
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`⛩ ${safeOpeningTitle(opening, id.toString())} ⛩`)
-    .setDescription(description || 'Not Set')
-    .addFields(
-      { name: 'ID', value: id.toString(), inline: true },
-      { name: 'Reward', value: openingData?.rewardPerBlock.toString(), inline: true },
-      { name: 'Application Stake', value: formatBalance(openingData?.stakeAmount.toString()), inline: true },
-    ), blockNumber, event);
-}
-
-export function getOpeningCancelledEmbed(id: OpeningId, opening: OpeningByIdQuery, blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`⛩ Opening ${safeOpeningTitle(opening, id.toString())} was cancelled⛩`)
-    , blockNumber, event);
-}
-
-export function getOpeningFilledEmbed(opening: OpeningByIdQuery, member: WorkerByIdQuery, blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(
-    new Discord.MessageEmbed()
-      .setTitle(
-        `🎉 🥳 👏🏻 ${member.workerByUniqueInput?.membership.handle} was hired for opening '${safeOpeningTitle(opening, getOpeningId(opening))}' 🎉 🥳 👏🏻`)
-    , blockNumber, event);
-}
-
-export function getAppliedOnOpeningEmbed(applicationId: ApplicationId,
-  opening: OpeningByIdQuery, applicant: MemberByIdQuery, blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`🏛 ${applicant.memberships[0].handle} applied to opening ${safeOpeningTitle(opening, getOpeningId(opening))}`)
-    .addFields(
-      { name: 'Application ID', value: applicationId.toString(), inline: true },
-      { name: 'Opening', value: getOpeningId(opening), inline: true },
-      { name: 'Member ID', value: `[${applicant.memberships[0].id}]`, inline: true },
-    ), blockNumber, event);
-}
-
-
-export function getWorkerRewardAmountUpdatedEmbed(reward: Balance, member: WorkerByIdQuery,
-  blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`💰💰💰 Salary of ${member.workerByUniqueInput?.membership.handle} updated`)
-    .addFields(
-      { name: 'Salary', value: formatBalance(reward.toString()), inline: true }
-    ), blockNumber, event);
-}
-
-export function getDiscretionarySpendingEmbed(spending: Balance, recipient: WorkersByAccountQuery,
-  blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`💰💰💰 User ${recipient.workers[0].membership.handle} was paid via discretionary budget spending`)
-    .addFields(
-      { name: 'Amount paid', value: formatBalance(spending.toString()), inline: true }
-    ), blockNumber, event);
-}
-
-export function getDiscretionarySpendingToNonWorkerAddressEmbed(spending: Balance, recipientAddress: string,
-  blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`💰💰💰 Discretionary budget spending was made`)
-    .addFields(
-      { name: 'Amount paid', value: formatBalance(spending.toString()), inline: true },
-      { name: 'Recipient', value: recipientAddress, inline: true }
-    ), blockNumber, event);
-}
-
-
-export function getWorkerRewardedEmbed(reward: Balance, member: WorkerByIdQuery, missed: boolean,
-  blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`💰💰💰 Remuneration${missed ? ' debt' : ''} for ${member.workerByUniqueInput?.membership.handle} paid successfully`)
-    .addFields(
-      { name: `Amount paid`, value: formatBalance(reward.toString()), inline: true }
-    ), blockNumber, event);
-}
-
-export function getLeaderSetEmbed(member: WorkerByIdQuery, blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed().setTitle(`🏛 ${member.workerByUniqueInput?.membership.handle} is a new Lead`), blockNumber, event);
-}
-
-export function getLeaderUnsetEmbed(blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed().setTitle(`🏛 Leader was unset`), blockNumber, event);
-}
-
-export function getWorkerTerminatedEmbed(member: WorkerByIdQuery, payload: EventWithBlock): Discord.MessageEmbed {
-  return getWorkerExitedOrTerminatedEmbed('been terminated', member, payload);
-}
-
-export function getWorkerExitedEmbed(member: WorkerByIdQuery, payload: EventWithBlock): Discord.MessageEmbed {
-  return getWorkerExitedOrTerminatedEmbed('exited', member, payload);
-}
-
-export function getWorkerExitedOrTerminatedEmbed(action: string, member: WorkerByIdQuery, payload: EventWithBlock): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`🏛 Worker ${member.workerByUniqueInput?.membership.handle} has ${action}`)
-    , payload.block, payload.event);
-}
-
-export function getApplicationWithdrawnEmbed(applicationId: ApplicationId, application: ApplicationByIdQuery,
-  blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`🏛 Application of ${application.workingGroupApplicationByUniqueInput?.applicant.handle} withdrawn`)
-    .addFields(
-      { name: 'Application ID', value: applicationId.toString() || 'Not Set', inline: true },
-      { name: 'Opening ID', value: application.workingGroupApplicationByUniqueInput?.openingId || 'Not Set', inline: true },
-    ), blockNumber, event);
-}
-
-export function getStakeUpdatedEmbed(stake: Balance | null, member: WorkerByIdQuery, action: string, blockNumber: number, event: EventRecord): Discord.MessageEmbed {
-
-  return addCommonProperties(new Discord.MessageEmbed()
-    .setTitle(`💰💰💰 ${member.workerByUniqueInput?.membership.handle}'s stake has been ${action}`)
-    .addFields(
-      { name: 'Amount', value: stake ? formatBalance(stake.toString()) : 'Not Set', inline: true }
-    ), blockNumber, event);
-}
-
-function addCommonProperties(embed: Discord.MessageEmbed, blockNumber: number, event: EventRecord) {
-  return embed.addFields(
-    { name: 'Block', value: blockNumber + '', inline: true },
-    { name: 'Tx', value: event.hash.toString(), inline: true },
-  )
-    .setColor(joystreamBlue)
-    .setTimestamp();
-}
-
-function safeOpeningTitle(opening: OpeningByIdQuery, id: string): string {
-  return opening.workingGroupOpeningByUniqueInput?.metadata.title || `Untitled #${id}`
-}
-function getOpeningId(opening: OpeningByIdQuery): string {
-  return opening.workingGroupOpeningByUniqueInput?.id || 'Not Set';
-}
-

+ 5 - 19
src/wg/lead.handler.ts

@@ -1,37 +1,23 @@
 import { WorkerId } from '@joystream/types/primitives';
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
+
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getLeaderSetEmbed, getLeaderUnsetEmbed } from './embeds';
 
 @Injectable()
 export class LeadHandler extends BaseEventHandler {
-
   @OnEvent('*.LeaderSet')
   async handleLeadSetEvent(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
     const leaderId = data[0] as WorkerId;
-    const leaderWorker = await this.queryNodeClient.workerById(`${section}-${leaderId.toString()}`);
-    this.channels[section].forEach((ch: TextChannel) =>
-      ch.send({
-        embeds: [getLeaderSetEmbed(leaderWorker, payload.block, payload.event)],
-      }));
+    const leaderWorker = await this.queryNodeClient.workerById(
+      `${section}-${leaderId.toString()}`,
+    );
   }
 
   @OnEvent('*.LeaderUnset')
   async handleLeadUnsetEvent(payload: EventWithBlock) {
     const { section } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
-    this.channels[section].forEach((ch: TextChannel) =>
-      ch.send({
-        embeds: [getLeaderUnsetEmbed(payload.block, payload.event)]
-      }));
   }
-}
+}

+ 8 - 29
src/wg/opening-added-or-cancelled.handler.ts

@@ -1,10 +1,9 @@
 import { OpeningId } from '@joystream/types/primitives';
 import { Injectable, Logger } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
+
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getOpeningAddedEmbed, getOpeningCancelledEmbed } from './embeds';
 
 @Injectable()
 export class OpeningAddedOrCancelledHandler extends BaseEventHandler {
@@ -22,42 +21,22 @@ export class OpeningAddedOrCancelledHandler extends BaseEventHandler {
 
   async handle(payload: EventWithBlock) {
     const { section, method, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
+
     const openingId = data[0] as OpeningId;
     const openingIdKey = `${section}-${openingId.toString()}`;
     this.logger.debug(openingIdKey);
 
-    const qnOpeningObject = await this.queryNodeClient.openingById(openingIdKey)
+    const qnOpeningObject = await this.queryNodeClient.openingById(
+      openingIdKey,
+    );
     if (!qnOpeningObject || !qnOpeningObject.workingGroupOpeningByUniqueInput) {
       this.logger.log('Opening not found in QN');
     } else {
       if (method === 'OpeningAdded') {
-        this.channels[section].forEach((ch: TextChannel) =>
-          ch.send({
-            embeds: [
-              getOpeningAddedEmbed(
-                openingId,
-                qnOpeningObject,
-                payload.block,
-                payload.event
-              ),
-            ],
-          }));
+        // TODO
       } else {
-        this.channels[section].forEach((ch: TextChannel) =>
-          ch.send({
-            embeds: [
-              getOpeningCancelledEmbed(
-                openingId,
-                qnOpeningObject,
-                payload.block,
-                payload.event
-              ),
-            ],
-          }));
+        // TODO
       }
     }
   }
-}
+}

+ 13 - 22
src/wg/opening-filled.handler.ts

@@ -1,37 +1,28 @@
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
+import { OpeningId, WorkerId } from '@joystream/types/primitives';
+
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getOpeningFilledEmbed } from './embeds';
-import { OpeningId, WorkerId } from '@joystream/types/primitives';
 
 @Injectable()
 export class OpeningFilledHandler extends BaseEventHandler {
-
   @OnEvent('*.OpeningFilled')
   async handleOpeningFilledEvent(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
+
     const filledOpeningId = data[0] as OpeningId;
-    const filledOpeningObject = await this.queryNodeClient.openingById(`${section}-${filledOpeningId.toString()}`);
-    const hiredWorkers = Object.values<WorkerId>(JSON.parse(data[1].toString()));
+    const filledOpeningObject = await this.queryNodeClient.openingById(
+      `${section}-${filledOpeningId.toString()}`,
+    );
+    const hiredWorkers = Object.values<WorkerId>(
+      JSON.parse(data[1].toString()),
+    );
 
     hiredWorkers.map(async (id, index, values) => {
-      const hiredWorker = await this.queryNodeClient.workerById(`${section}-${id.toString()}`);
-      this.channels[section].forEach((ch: TextChannel) =>
-        ch.send({
-          embeds: [
-            getOpeningFilledEmbed(
-              filledOpeningObject,
-              hiredWorker,
-              payload.block,
-              payload.event
-            ),
-          ],
-        }));
+      const hiredWorker = await this.queryNodeClient.workerById(
+        `${section}-${id.toString()}`,
+      );
     });
   }
-}
+}

+ 10 - 23
src/wg/reward-paid.handler.ts

@@ -1,37 +1,24 @@
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
-import { EventWithBlock } from 'src/types';
-import { BaseEventHandler } from './base-event.handler';
 import { WorkerId } from '@joystream/types/primitives';
-import { getWorkerRewardedEmbed } from './embeds';
 import { Balance } from '@polkadot/types/interfaces';
+
+import { EventWithBlock } from 'src/types';
+import { BaseEventHandler } from './base-event.handler';
 import { PalletWorkingGroupRewardPaymentType } from '@polkadot/types/lookup';
 
 @Injectable()
 export class RewardPaidHandler extends BaseEventHandler {
-
   @OnEvent('*.RewardPaid')
   async handleRewardPaidEvent(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
+
     const paidWorkerId = data[0] as WorkerId;
-    const paidWorkerAffected = await this.queryNodeClient.workerById(`${section}-${paidWorkerId.toString()}`);
+    const paidWorkerAffected = await this.queryNodeClient.workerById(
+      `${section}-${paidWorkerId.toString()}`,
+    );
     const paidReward = data[2] as Balance;
-    const isRewardMissed = (data[3] as PalletWorkingGroupRewardPaymentType).isMissedReward;
-    this.channels[section].forEach((ch: TextChannel) =>
-      ch.send({
-        embeds: [
-          getWorkerRewardedEmbed(
-            paidReward,
-            paidWorkerAffected,
-            isRewardMissed,
-            payload.block,
-            payload.event
-          ),
-        ],
-      }));
+    const isRewardMissed = (data[3] as PalletWorkingGroupRewardPaymentType)
+      .isMissedReward;
   }
-}
+}

+ 11 - 23
src/wg/reward-updated.handler.ts

@@ -1,36 +1,24 @@
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
-import { EventWithBlock } from 'src/types';
-import { BaseEventHandler } from './base-event.handler';
 import { WorkerId } from '@joystream/types/primitives';
-import { getWorkerRewardAmountUpdatedEmbed } from './embeds';
 import { Balance } from '@polkadot/types/interfaces';
-
 import type { Option } from '@polkadot/types';
 
+import { EventWithBlock } from 'src/types';
+import { BaseEventHandler } from './base-event.handler';
+
 @Injectable()
 export class RewardUpdatedHandler extends BaseEventHandler {
-
   @OnEvent('*.WorkerRewardAmountUpdated')
   async handleRewardUpdatedEvent(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
+
     const workerId = data[0] as WorkerId;
-    const workerAffected = await this.queryNodeClient.workerById(`${section}-${workerId.toString()}`);
-    const reward = (data[1] as Option<Balance>).unwrapOr(0 as unknown as Balance);
-    this.channels[section].forEach((ch: TextChannel) =>
-      ch.send({
-        embeds: [
-          getWorkerRewardAmountUpdatedEmbed(
-            reward,
-            workerAffected,
-            payload.block,
-            payload.event
-          ),
-        ],
-      }));
+    const workerAffected = await this.queryNodeClient.workerById(
+      `${section}-${workerId.toString()}`,
+    );
+    const reward = (data[1] as Option<Balance>).unwrapOr(
+      0 as unknown as Balance,
+    );
   }
-}
+}

+ 0 - 66
src/wg/simulate.ts

@@ -1,66 +0,0 @@
-import { Client, Intents } from "discord.js";
-// import { ApiPromise } from "@polkadot/api";
-// import { EventRecord } from "@polkadot/types/interfaces";
-// import { wsLocation } from "../../config";
-// import { connectApi, getBlockHash, getEvents } from "../util";
-// import { getDiscordChannels } from "../util";
-// import { DiscordChannels } from "../types";
-// import { processGroupEvents } from "./wg.handlers";
-// import { RetryableGraphQLClient } from "src/gql/graphql.client";
-
-// const eventsMapping = {
-//   // BudgetRefill: 28800,
-//   // BudgetSet: 978,
-//   UpdatedWorkingGroupBudget: 103053,
-//   // OpeningFilled: 74353,
-//   // OpeningAdded: 43286,
-//   // OpeningAdded2: 125834,
-//   // OpeningCancelled: 602245,
-//   // RewardPaid: 608580,
-//   // MissedReward: 115920,
-//   // WorkerRewardAmountUpdated: 112393,
-//   // WorkerRewardAmountUpdated2: 117500,
-//   // WorkerRoleAccountUpdated: 44513,
-//   // RewardPayment: 57600,
-//   // NewMissedRewardLevelReached: 57640,
-//   // AppliedOnOpening: 43405,
-//   // AppliedOnOpening2: 83269,
-//   ApplicationWithdrawn: 514629,
-//   // StakeIncreased: 4264798,
-//   // StakeDecreased: 4264862,
-// // TerminatedLeader: 4282370,
-//   // LeaderSet: 45047,
-//   // StakeSlashed: 498971,
-//   // TerminatedWorker: 4908750
-// };
-
-const discordBotToken = process.env.TOKEN || undefined; // environment variable TOKEN must be set
-
-;(async () => {
-
-  const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
-
-  client.once("ready", async () => {
-    console.log('Discord.js client ready');
-    // const channels: DiscordChannels = await getDiscordChannels(client);
-    // const api: ApiPromise = await connectApi(wsLocation);
-    // await api.isReady;
-    // Object.values(eventsMapping).forEach((block: number) => {
-    //   console.log(`Processing ${block}`);
-    //   getBlockHash(api, block).then((hash) =>
-    //     getEvents(api, hash).then((events: EventRecord[]) =>
-    //       // processGroupEvents(block, events, channels)
-    //     )
-    //   )
-    // });
-  });
-
-  client.on("debug", console.log);
-  client.on("error", console.error);
-  client.on("apiResponse", console.log);
-  client.on("apiRequest", console.error);
-
-  client.login(discordBotToken).then(async () => {
-    console.log("Bot logged in successfully");
-  });
-})()

+ 6 - 21
src/wg/stake.handler.ts

@@ -2,14 +2,12 @@ import { Balance } from '@polkadot/types/interfaces';
 import { WorkerId } from '@joystream/types/primitives';
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
+
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getStakeUpdatedEmbed } from './embeds';
 
 @Injectable()
 export class StakeHandler extends BaseEventHandler {
-  
   @OnEvent('*.StakeIncreased')
   async handleIncrease(payload: EventWithBlock) {
     await this.handle(payload);
@@ -27,24 +25,11 @@ export class StakeHandler extends BaseEventHandler {
 
   async handle(payload: EventWithBlock) {
     const { section, method, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
+
     const stakeWorkerId = data[0] as WorkerId;
-    const stakeWorker = await this.queryNodeClient.workerById(`${section}-${stakeWorkerId.toString()}`);
+    const stakeWorker = await this.queryNodeClient.workerById(
+      `${section}-${stakeWorkerId.toString()}`,
+    );
     const stake = data[1] as Balance;
-
-    this.channels[section].forEach((ch: TextChannel) =>
-      ch.send({
-        embeds: [
-          getStakeUpdatedEmbed(
-            stake,
-            stakeWorker,
-            method.replace('Stake', ''),
-            payload.block,
-            payload.event
-          ),
-        ],
-      }));
   }
-}
+}

+ 6 - 17
src/wg/termination.handler.ts

@@ -1,14 +1,12 @@
 import { WorkerId } from '@joystream/types/primitives';
 import { Injectable } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
+
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getWorkerTerminatedEmbed } from './embeds';
 
 @Injectable()
 export class TerminationHandler extends BaseEventHandler {
-
   @OnEvent('*.TerminatedWorker')
   async handleWorkerTerminationEvent(payload: EventWithBlock) {
     this.handle(payload);
@@ -21,20 +19,11 @@ export class TerminationHandler extends BaseEventHandler {
 
   async handle(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
+
     const terminatedId = data[0] as WorkerId;
     const terminatedWorkerKey = `${section}-${terminatedId.toString()}`;
-    const terminatedIdWorker = await this.queryNodeClient.workerById(terminatedWorkerKey);
-    this.channels[section].forEach((ch: TextChannel) =>
-      ch.send({
-        embeds: [
-          getWorkerTerminatedEmbed(
-            terminatedIdWorker,
-            payload
-          ),
-        ],
-      }));
+    const terminatedIdWorker = await this.queryNodeClient.workerById(
+      terminatedWorkerKey,
+    );
   }
-}
+}

+ 1 - 6
src/wg/wg.module.ts

@@ -1,5 +1,4 @@
 import { Module } from '@nestjs/common';
-import { DiscordModule } from '@discord-nestjs/core';
 import { ConfigModule } from '@nestjs/config';
 import { PioneerGraphQLModule } from 'src/gql/pioneer.module';
 import { ApplicationWithdrawnHandler } from './application-withdrawn.handler';
@@ -17,11 +16,7 @@ import { WorkingGroupService } from './wg.service';
 import { BudgetSpendingHandler } from './budget-spending.handler';
 
 @Module({
-  imports: [
-    DiscordModule.forFeature(),
-    ConfigModule.forRoot(),
-    PioneerGraphQLModule,
-  ],
+  imports: [ConfigModule.forRoot(), PioneerGraphQLModule],
   providers: [
     BudgetSetHandler,
     BudgetUpdatedHandler,

+ 2 - 43
src/wg/wg.service.ts

@@ -1,51 +1,10 @@
-import {
-  identityValidatedRole,
-  wgToRoleMap,
-  councilMemberRole,
-} from '../../config';
-import { findServerRole } from '../util';
-import { banner } from '../banner';
 import { Injectable, Logger } from '@nestjs/common';
-import { InjectDiscordClient, Once } from '@discord-nestjs/core';
-import { Client } from 'discord.js';
+
 import { ConfigService } from '@nestjs/config';
 
 @Injectable()
 export class WorkingGroupService {
   private readonly logger = new Logger(WorkingGroupService.name);
 
-  constructor(
-    @InjectDiscordClient()
-    private readonly client: Client,
-    private readonly configService: ConfigService,
-  ) {}
-
-  @Once('ready')
-  async onReady(): Promise<void> {
-    this.logger.log(banner);
-    this.logger.log(
-      `Bot online. Current server[s]: ${(
-        await this.client.guilds.fetch({ limit: 10 })
-      )
-        .map((g) => g.name)
-        .join(',')}`,
-    );
-
-    // check the config settings agaist the server specified as environment variable
-    this.selfCheck();
-  }
-
-  private selfCheck() {
-    const serverToCheck = this.configService.get('DISCORD_SERVER');
-    const rolesToCheck: string[] = [
-      identityValidatedRole,
-      councilMemberRole,
-      ...Object.values(wgToRoleMap),
-    ];
-    rolesToCheck.forEach(async (role: string, ix: number, vals: string[]) => {
-      if (!(await findServerRole(this.client, serverToCheck, role))) {
-        this.logger.error(`Configured role [${role}] not found`);
-      }
-    });
-  }
+  constructor(private readonly configService: ConfigService) {}
 }

+ 2 - 15
src/wg/worker-exited.handler.ts

@@ -1,10 +1,9 @@
 import { WorkerId } from '@joystream/types/primitives';
 import { Injectable, Logger } from '@nestjs/common';
 import { OnEvent } from '@nestjs/event-emitter';
-import { TextChannel } from 'discord.js';
+
 import { EventWithBlock } from 'src/types';
 import { BaseEventHandler } from './base-event.handler';
-import { getWorkerExitedEmbed } from './embeds';
 
 @Injectable()
 export class WorkerExitedHandler extends BaseEventHandler {
@@ -13,22 +12,10 @@ export class WorkerExitedHandler extends BaseEventHandler {
   @OnEvent('*.WorkerExited')
   async handleWorkerExitedEvent(payload: EventWithBlock) {
     const { section, data } = payload.event.event;
-    if (!this.checkChannel(section)) {
-      return;
-    }
     const exitedId = data[0] as WorkerId;
     const exitedWorkerKey = `${section}-${exitedId.toString()}`;
     this.logger.debug(exitedWorkerKey);
 
     const exitedMember = await this.queryNodeClient.workerById(exitedWorkerKey);
-    this.channels[section].forEach((ch: TextChannel) =>
-      ch.send({
-        embeds: [
-          getWorkerExitedEmbed(
-            exitedMember,
-            payload
-          ),
-        ],
-      }));
   }
-}
+}

+ 8 - 13
tsconfig.json

@@ -5,27 +5,22 @@
     "strict": true,
     "sourceMap": true,
     "noImplicitAny": true,
-    "noUnusedLocals": true,
+    "noUnusedLocals": false,
     "noImplicitReturns": true,
     "moduleResolution": "node",
-    "useDefineForClassFields": true, /* false ruins the discord commands (their parameters are not registering :( */
-    "allowSyntheticDefaultImports": true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
-    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
-    "experimentalDecorators": true,           /* Enables experimental support for ES7 decorators. */
+    "useDefineForClassFields": true /* false ruins the discord commands (their parameters are not registering :( */,
+    "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
+    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
+    "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
     "emitDecoratorMetadata": true,
     "declaration": true,
     "resolveJsonModule": true,
     "skipLibCheck": true,
-    "types" : [
-      "node"
-    ],
+    "types": ["node"],
     "forceConsistentCasingInFileNames": true,
     "baseUrl": ".",
-    "typeRoots": [
-      "./node_modules/@polkadot/ts",
-      "./node_modules/@types"
-    ],
+    "typeRoots": ["./node_modules/@polkadot/ts", "./node_modules/@types"],
     "declarationDir": "./dist",
     "outDir": "./dist"
   }
-}
+}

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff