Skip to content

Commit

Permalink
feat: add inital support for orgs
Browse files Browse the repository at this point in the history
- List membership (public or "admin_mode")
- List applicants (admin_mode only)
  • Loading branch information
Superd22 committed Jan 10, 2019
1 parent de13049 commit f56fc15
Show file tree
Hide file tree
Showing 14 changed files with 404 additions and 21 deletions.
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"draft-js": "^0.10.0",
"draft-js-emoji-plugin": "^2.0.0-rc9",
"fs-extra": "^7.0.1",
"node-html-parser": "^1.1.11",
"popsicle": "^10.0.1",
"react": "^16.7.0",
"react-dom": "^16.7.0",
Expand Down
19 changes: 19 additions & 0 deletions spec/org.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Container } from "typedi";
import { SpectrumLobby } from "./../src/";
import { SpectrumChannel } from "./../src/";
import { SpectrumBroadcaster } from "./../src/";
import { SpectrumCommunity, SpectrumCommands } from "../src/";
import { TestInstance } from "./_.instance";
import { TestShared } from "./_.shared";

import {} from "jasmine";
import { SpectrumCommand } from "../src/Spectrum/components/api/decorators/spectrum-command.decorator";

describe("Organizations", () => {
describe(`Management`, () => {
describe(`Member list`, () => {
it(`Should list memberlist`);
it(`Should search through memberlist`);
})
})
});
1 change: 1 addition & 0 deletions src/RSI/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
*/ /** */

export { RSIService as RSI } from "./services/rsi.service";
export * from "./orgs";
12 changes: 6 additions & 6 deletions src/RSI/interfaces/RSIApiResponse.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* @module RSI
*/ /** */

export interface RSIApiResponse {
success:number,
data:any,
code:string;
msg:string;
}
export interface RSIApiResponse<T = any> {
success: number;
data: T;
code: string;
msg: string;
}
1 change: 1 addition & 0 deletions src/RSI/interfaces/omit.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
177 changes: 177 additions & 0 deletions src/RSI/orgs/entities/organisation.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { GetApplicants } from "./../interfaces/rsi/get-applicants.interface";
import { GetApplicantsParams } from "./../interfaces/rsi/get-applicants-params.interface";
import {
GetOrgMembersOpts,
OrgMemberRank,
OrgMemberVisibility
} from "../interfaces/api/get-org-members-params.interface";
import { Container } from "typedi";
import { RSIService } from "../../services/rsi.service";
import { GetOrgMembers, OrgMember } from "../interfaces/api/get-org-members.interface";
import { GetOrgMembersOptions } from "../interfaces/api/get-org-members-params.interface";
import { parse, HTMLElement } from "node-html-parser";
import { OrgApplicant } from "../interfaces/rsi/get-applicants.interface";

/**
* Represents an RSI Organisation
*/
export class Organisation {
/** main rsi service */
protected _rsi: RSIService = Container.get(RSIService);

/**
* @param SSID unique SSID of the organisation
*/
constructor(public readonly SSID: string) {}

/**
* Get the list of members for this organisation that can be seen by everyone
* this does not require any specific privilege.
*
* this will **NOT** return "HIDDEN" members and will return no personal info for "REDACTED" members
*
* @param options fetch options
* @see `getMembers()` If you have memberlist privilege and want all the members
*/
public async getPublicMembers(): Promise<OrgMember[]>;
public async getPublicMembers(options: GetOrgMembersOptions): Promise<OrgMember[]>;
public async getPublicMembers(options?: GetOrgMembersOptions): Promise<OrgMember[]> {
const res = await this._rsi.post<GetOrgMembers>(`api/orgs/getOrgMembers`, {
...options,
symbol: this.SSID
});

return this.buildMembersReturn(res.data, options);
}

/**
* Get the list of members for this organisation.
* You **will** need memberlist privilege for this to work.
*
* @param options fetch options
* @see `getPublicMembers()` for a list of public members if you do not have privilege
*/
public async getMembers(): Promise<OrgMember[]>;
public async getMembers(options: GetOrgMembersOptions): Promise<OrgMember[]>;
public async getMembers(options?: GetOrgMembersOptions): Promise<OrgMember[]> {
const res = await this._rsi.post<GetOrgMembers>(`api/orgs/getOrgMembers`, {
...options,
admin_mode: 1,
symbol: this.SSID
});

return this.buildMembersReturn(res.data, options, true);
}

/**
* Get the list of current applicants
*
* will fetch the first 500 applicants by default.
*/
public async getApplicants(): Promise<GetApplicants>;
public async getApplicants(options: GetApplicantsParams): Promise<GetApplicants>;
public async getApplicants(
options: GetApplicantsParams = { page: 1, pagesize: 500 }
): Promise<GetApplicants> {
const res = await this._rsi.navigate(
`orgs/${this.SSID}/admin/applications?page=${options.page}&pagesize=${options.pagesize}`
);

return this.parseApplicants(res);
}

/**
* Parse the HTML returned by getApplicants() into an array of OrgApplicant
*/
protected parseApplicants(resHTML: string): Array<OrgApplicant> {
const root = parse(resHTML);


return root
.querySelectorAll("ul.applicants-listing li.clearfix")
.map((li: HTMLElement) => {
const applicant: OrgApplicant = {
id: Number(
(li.querySelectorAll("div.player-cell")[0] as HTMLElement).attributes[
"data-app-id"
]
),
// no, this is not a typo, they do display the HANDLE in a .nick
handle: li.querySelectorAll("span.nick")[0].text,
nick: li.querySelectorAll("a.name")[0].text,
message: li.querySelectorAll("span.message")[0].text
};

return applicant;
});
}

protected async buildMembersReturn(
res: GetOrgMembers,
opts?: GetOrgMembersOptions,
admin = false
) {
const firstPassMembers = this.parseMembers(res);

if (opts && (opts as GetOrgMembersOpts).allMembers) {
if (firstPassMembers.length < res.totalrows) {
/**
* We have to make multiple calls because rsi api
* does not support a pagesize != 32 ...
*/
for (let i = 2; i <= Math.ceil(res.totalrows / 32); i++) {
// We need to fetch again :(
const res2 = await this._rsi.post<GetOrgMembers>(`api/orgs/getOrgMembers`, {
...opts,
page: i,
admin_mode: admin ? 1 : undefined,
symbol: this.SSID
});
firstPassMembers.push(...this.parseMembers(res2.data));
}
}
}

return firstPassMembers;
}

/**
* Parse the HTML of a getOrgMembers calls into an OrgMember array
* @param res the res of getOrgMembers call
*/
protected parseMembers(res: GetOrgMembers) {
const root = parse(res.html);

const members = root.querySelectorAll("li").map(li => {
const el = li as HTMLElement;

const user: OrgMember = {
id: Number(el.attributes["data-member-id"]),
handle: el.attributes["data-member-nickname"],
monicker: el.attributes["data-member-displayname"],
avatar:
el.attributes["data-member-avatar"].length > 5
? el.attributes["data-member-avatar"]
: null,
rank: null,
visibility: null
};

const [_, rank] = (el.querySelector("span.ranking-stars") as HTMLElement).classNames
.join(" ")
.match(/data([0-9])/);

user["rank"] = OrgMemberRank[rank];

const [_1, visibility] = (el.querySelector(
"span.visibility"
) as HTMLElement).text.match(/Membership: (.*)/);

user["visibility"] = visibility.substr(0, 1).toUpperCase() as OrgMemberVisibility;

return user;
});

return members;
}
}
7 changes: 7 additions & 0 deletions src/RSI/orgs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Container } from "typedi";
export { OrgsService } from "./services/orgs.service";
import { OrgsService } from "./services/orgs.service";

const organisations = Container.get(OrgsService);

export { organisations };
79 changes: 79 additions & 0 deletions src/RSI/orgs/interfaces/api/get-org-members-params.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Omit } from "../../../interfaces/omit.type";

/**
* Params for GetOrgMembers
*/
export interface GetOrgMembersParams {
/** if we want to do the search as an "admin" (ie: with privilege and see HIDDEN) */
admin_mode?: 1;
/** filter by member ranks (1 being the higest, 6 the lowest) */
rank?: OrgMemberRank;
/** filter by member rank () */
role?: OrgMemberRole;
/** filter by member main/affiliate status (1 = Main only | 0 = Affiliate only) */
main_org?: 0 | 1;
/** filter by member monicker/handle */
search?: string;
/** filter by member visibility */
visibility?: OrgMemberVisibility;
/** SSID of the org to search for */
symbol: string;
}

export interface PaginatedGetOrgMembersParam extends GetOrgMembersParams {
page: number;
pagesize: number;
}

export type GetOrgMembersPaginatedOpts = Omit<PaginatedGetOrgMembersParam, "admin_mode" | "symbol">;
export type GetOrgMembersOpts = Omit<GetOrgMembersParams, "admin_mode" | "symbol"> & {
/**
* return every members in the org
*
* **\/!\ due to an RSI's API limitation, this will generate ceil(OrgMembers / 32) API calls.**
* This is therefore a "little" slow.
*
* If you only want the total **count** of members and not their infos, see GetOrgsMembers.totalrows
*/
allMembers: boolean;
};

/**
* Options for
*/
export type GetOrgMembersOptions = GetOrgMembersOpts | GetOrgMembersPaginatedOpts;

/**
* Available ranks in an org for members
*/
export enum OrgMemberRank {
FIVE_STARS = 1,
FOUR_STARS = 2,
THREE_STARS = 3,
TWO_STARS = 4,
ONE_STAR = 5,
ZERO_STAR = 6
}

/**
* Available visibilities in an org for members
*/
export enum OrgMemberVisibility {
VISIBLE = "V",
REDACTED = "R",
HIDDEN = "H"
}

/**
* Available roles in an org for main members
*/
export enum OrgMemberRole {
/**can do anything, from recruiting to customization, to simply disbanding the organization*/
Owner = 1,
/** can send out invites to the org, and accept or deny applicants */
Recruitment = 2,
/** can manage the org’s members, and their roles/ranks, as well as moderating the Org’s private Chat channel. */
Officer = 3,
/**can change the org’s public appearance, official texts, history, manifesto and charter. */
Marketing = 4
}
23 changes: 23 additions & 0 deletions src/RSI/orgs/interfaces/api/get-org-members.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { OrgMemberRank, OrgMemberVisibility } from "./get-org-members-params.interface";

/**
* Raw search result when calling getorgmembers
*/
export interface GetOrgMembers {
/** total amount of items that matched the search */
totalrows: number;
/** html containing the list of members in the org */
html: string;
}

/**
* Parsed return from GetOrgMembers call
*/
export interface OrgMember {
id: number;
handle: string;
monicker: string;
avatar: string;
rank: OrgMemberRank;
visibility: OrgMemberVisibility;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface GetApplicantsParams {
page: number;
pagesize: number;
}
9 changes: 9 additions & 0 deletions src/RSI/orgs/interfaces/rsi/get-applicants.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type GetApplicants = Array<OrgApplicant>;

export interface OrgApplicant {
id: number;
handle: string;
nick: string;
/** application message filled by the applicant */
message: string;
}
Loading

0 comments on commit f56fc15

Please sign in to comment.