Skip to content

Commit f56fc15

Browse files
committed
feat: add inital support for orgs
- List membership (public or "admin_mode") - List applicants (admin_mode only)
1 parent de13049 commit f56fc15

File tree

14 files changed

+404
-21
lines changed

14 files changed

+404
-21
lines changed

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"draft-js": "^0.10.0",
2323
"draft-js-emoji-plugin": "^2.0.0-rc9",
2424
"fs-extra": "^7.0.1",
25+
"node-html-parser": "^1.1.11",
2526
"popsicle": "^10.0.1",
2627
"react": "^16.7.0",
2728
"react-dom": "^16.7.0",

spec/org.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Container } from "typedi";
2+
import { SpectrumLobby } from "./../src/";
3+
import { SpectrumChannel } from "./../src/";
4+
import { SpectrumBroadcaster } from "./../src/";
5+
import { SpectrumCommunity, SpectrumCommands } from "../src/";
6+
import { TestInstance } from "./_.instance";
7+
import { TestShared } from "./_.shared";
8+
9+
import {} from "jasmine";
10+
import { SpectrumCommand } from "../src/Spectrum/components/api/decorators/spectrum-command.decorator";
11+
12+
describe("Organizations", () => {
13+
describe(`Management`, () => {
14+
describe(`Member list`, () => {
15+
it(`Should list memberlist`);
16+
it(`Should search through memberlist`);
17+
})
18+
})
19+
});

src/RSI/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
*/ /** */
88

99
export { RSIService as RSI } from "./services/rsi.service";
10+
export * from "./orgs";

src/RSI/interfaces/RSIApiResponse.interface.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
* @module RSI
33
*/ /** */
44

5-
export interface RSIApiResponse {
6-
success:number,
7-
data:any,
8-
code:string;
9-
msg:string;
10-
}
5+
export interface RSIApiResponse<T = any> {
6+
success: number;
7+
data: T;
8+
code: string;
9+
msg: string;
10+
}

src/RSI/interfaces/omit.type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { GetApplicants } from "./../interfaces/rsi/get-applicants.interface";
2+
import { GetApplicantsParams } from "./../interfaces/rsi/get-applicants-params.interface";
3+
import {
4+
GetOrgMembersOpts,
5+
OrgMemberRank,
6+
OrgMemberVisibility
7+
} from "../interfaces/api/get-org-members-params.interface";
8+
import { Container } from "typedi";
9+
import { RSIService } from "../../services/rsi.service";
10+
import { GetOrgMembers, OrgMember } from "../interfaces/api/get-org-members.interface";
11+
import { GetOrgMembersOptions } from "../interfaces/api/get-org-members-params.interface";
12+
import { parse, HTMLElement } from "node-html-parser";
13+
import { OrgApplicant } from "../interfaces/rsi/get-applicants.interface";
14+
15+
/**
16+
* Represents an RSI Organisation
17+
*/
18+
export class Organisation {
19+
/** main rsi service */
20+
protected _rsi: RSIService = Container.get(RSIService);
21+
22+
/**
23+
* @param SSID unique SSID of the organisation
24+
*/
25+
constructor(public readonly SSID: string) {}
26+
27+
/**
28+
* Get the list of members for this organisation that can be seen by everyone
29+
* this does not require any specific privilege.
30+
*
31+
* this will **NOT** return "HIDDEN" members and will return no personal info for "REDACTED" members
32+
*
33+
* @param options fetch options
34+
* @see `getMembers()` If you have memberlist privilege and want all the members
35+
*/
36+
public async getPublicMembers(): Promise<OrgMember[]>;
37+
public async getPublicMembers(options: GetOrgMembersOptions): Promise<OrgMember[]>;
38+
public async getPublicMembers(options?: GetOrgMembersOptions): Promise<OrgMember[]> {
39+
const res = await this._rsi.post<GetOrgMembers>(`api/orgs/getOrgMembers`, {
40+
...options,
41+
symbol: this.SSID
42+
});
43+
44+
return this.buildMembersReturn(res.data, options);
45+
}
46+
47+
/**
48+
* Get the list of members for this organisation.
49+
* You **will** need memberlist privilege for this to work.
50+
*
51+
* @param options fetch options
52+
* @see `getPublicMembers()` for a list of public members if you do not have privilege
53+
*/
54+
public async getMembers(): Promise<OrgMember[]>;
55+
public async getMembers(options: GetOrgMembersOptions): Promise<OrgMember[]>;
56+
public async getMembers(options?: GetOrgMembersOptions): Promise<OrgMember[]> {
57+
const res = await this._rsi.post<GetOrgMembers>(`api/orgs/getOrgMembers`, {
58+
...options,
59+
admin_mode: 1,
60+
symbol: this.SSID
61+
});
62+
63+
return this.buildMembersReturn(res.data, options, true);
64+
}
65+
66+
/**
67+
* Get the list of current applicants
68+
*
69+
* will fetch the first 500 applicants by default.
70+
*/
71+
public async getApplicants(): Promise<GetApplicants>;
72+
public async getApplicants(options: GetApplicantsParams): Promise<GetApplicants>;
73+
public async getApplicants(
74+
options: GetApplicantsParams = { page: 1, pagesize: 500 }
75+
): Promise<GetApplicants> {
76+
const res = await this._rsi.navigate(
77+
`orgs/${this.SSID}/admin/applications?page=${options.page}&pagesize=${options.pagesize}`
78+
);
79+
80+
return this.parseApplicants(res);
81+
}
82+
83+
/**
84+
* Parse the HTML returned by getApplicants() into an array of OrgApplicant
85+
*/
86+
protected parseApplicants(resHTML: string): Array<OrgApplicant> {
87+
const root = parse(resHTML);
88+
89+
90+
return root
91+
.querySelectorAll("ul.applicants-listing li.clearfix")
92+
.map((li: HTMLElement) => {
93+
const applicant: OrgApplicant = {
94+
id: Number(
95+
(li.querySelectorAll("div.player-cell")[0] as HTMLElement).attributes[
96+
"data-app-id"
97+
]
98+
),
99+
// no, this is not a typo, they do display the HANDLE in a .nick
100+
handle: li.querySelectorAll("span.nick")[0].text,
101+
nick: li.querySelectorAll("a.name")[0].text,
102+
message: li.querySelectorAll("span.message")[0].text
103+
};
104+
105+
return applicant;
106+
});
107+
}
108+
109+
protected async buildMembersReturn(
110+
res: GetOrgMembers,
111+
opts?: GetOrgMembersOptions,
112+
admin = false
113+
) {
114+
const firstPassMembers = this.parseMembers(res);
115+
116+
if (opts && (opts as GetOrgMembersOpts).allMembers) {
117+
if (firstPassMembers.length < res.totalrows) {
118+
/**
119+
* We have to make multiple calls because rsi api
120+
* does not support a pagesize != 32 ...
121+
*/
122+
for (let i = 2; i <= Math.ceil(res.totalrows / 32); i++) {
123+
// We need to fetch again :(
124+
const res2 = await this._rsi.post<GetOrgMembers>(`api/orgs/getOrgMembers`, {
125+
...opts,
126+
page: i,
127+
admin_mode: admin ? 1 : undefined,
128+
symbol: this.SSID
129+
});
130+
firstPassMembers.push(...this.parseMembers(res2.data));
131+
}
132+
}
133+
}
134+
135+
return firstPassMembers;
136+
}
137+
138+
/**
139+
* Parse the HTML of a getOrgMembers calls into an OrgMember array
140+
* @param res the res of getOrgMembers call
141+
*/
142+
protected parseMembers(res: GetOrgMembers) {
143+
const root = parse(res.html);
144+
145+
const members = root.querySelectorAll("li").map(li => {
146+
const el = li as HTMLElement;
147+
148+
const user: OrgMember = {
149+
id: Number(el.attributes["data-member-id"]),
150+
handle: el.attributes["data-member-nickname"],
151+
monicker: el.attributes["data-member-displayname"],
152+
avatar:
153+
el.attributes["data-member-avatar"].length > 5
154+
? el.attributes["data-member-avatar"]
155+
: null,
156+
rank: null,
157+
visibility: null
158+
};
159+
160+
const [_, rank] = (el.querySelector("span.ranking-stars") as HTMLElement).classNames
161+
.join(" ")
162+
.match(/data([0-9])/);
163+
164+
user["rank"] = OrgMemberRank[rank];
165+
166+
const [_1, visibility] = (el.querySelector(
167+
"span.visibility"
168+
) as HTMLElement).text.match(/Membership: (.*)/);
169+
170+
user["visibility"] = visibility.substr(0, 1).toUpperCase() as OrgMemberVisibility;
171+
172+
return user;
173+
});
174+
175+
return members;
176+
}
177+
}

src/RSI/orgs/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Container } from "typedi";
2+
export { OrgsService } from "./services/orgs.service";
3+
import { OrgsService } from "./services/orgs.service";
4+
5+
const organisations = Container.get(OrgsService);
6+
7+
export { organisations };
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Omit } from "../../../interfaces/omit.type";
2+
3+
/**
4+
* Params for GetOrgMembers
5+
*/
6+
export interface GetOrgMembersParams {
7+
/** if we want to do the search as an "admin" (ie: with privilege and see HIDDEN) */
8+
admin_mode?: 1;
9+
/** filter by member ranks (1 being the higest, 6 the lowest) */
10+
rank?: OrgMemberRank;
11+
/** filter by member rank () */
12+
role?: OrgMemberRole;
13+
/** filter by member main/affiliate status (1 = Main only | 0 = Affiliate only) */
14+
main_org?: 0 | 1;
15+
/** filter by member monicker/handle */
16+
search?: string;
17+
/** filter by member visibility */
18+
visibility?: OrgMemberVisibility;
19+
/** SSID of the org to search for */
20+
symbol: string;
21+
}
22+
23+
export interface PaginatedGetOrgMembersParam extends GetOrgMembersParams {
24+
page: number;
25+
pagesize: number;
26+
}
27+
28+
export type GetOrgMembersPaginatedOpts = Omit<PaginatedGetOrgMembersParam, "admin_mode" | "symbol">;
29+
export type GetOrgMembersOpts = Omit<GetOrgMembersParams, "admin_mode" | "symbol"> & {
30+
/**
31+
* return every members in the org
32+
*
33+
* **\/!\ due to an RSI's API limitation, this will generate ceil(OrgMembers / 32) API calls.**
34+
* This is therefore a "little" slow.
35+
*
36+
* If you only want the total **count** of members and not their infos, see GetOrgsMembers.totalrows
37+
*/
38+
allMembers: boolean;
39+
};
40+
41+
/**
42+
* Options for
43+
*/
44+
export type GetOrgMembersOptions = GetOrgMembersOpts | GetOrgMembersPaginatedOpts;
45+
46+
/**
47+
* Available ranks in an org for members
48+
*/
49+
export enum OrgMemberRank {
50+
FIVE_STARS = 1,
51+
FOUR_STARS = 2,
52+
THREE_STARS = 3,
53+
TWO_STARS = 4,
54+
ONE_STAR = 5,
55+
ZERO_STAR = 6
56+
}
57+
58+
/**
59+
* Available visibilities in an org for members
60+
*/
61+
export enum OrgMemberVisibility {
62+
VISIBLE = "V",
63+
REDACTED = "R",
64+
HIDDEN = "H"
65+
}
66+
67+
/**
68+
* Available roles in an org for main members
69+
*/
70+
export enum OrgMemberRole {
71+
/**can do anything, from recruiting to customization, to simply disbanding the organization*/
72+
Owner = 1,
73+
/** can send out invites to the org, and accept or deny applicants */
74+
Recruitment = 2,
75+
/** can manage the org’s members, and their roles/ranks, as well as moderating the Org’s private Chat channel. */
76+
Officer = 3,
77+
/**can change the org’s public appearance, official texts, history, manifesto and charter. */
78+
Marketing = 4
79+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { OrgMemberRank, OrgMemberVisibility } from "./get-org-members-params.interface";
2+
3+
/**
4+
* Raw search result when calling getorgmembers
5+
*/
6+
export interface GetOrgMembers {
7+
/** total amount of items that matched the search */
8+
totalrows: number;
9+
/** html containing the list of members in the org */
10+
html: string;
11+
}
12+
13+
/**
14+
* Parsed return from GetOrgMembers call
15+
*/
16+
export interface OrgMember {
17+
id: number;
18+
handle: string;
19+
monicker: string;
20+
avatar: string;
21+
rank: OrgMemberRank;
22+
visibility: OrgMemberVisibility;
23+
}

0 commit comments

Comments
 (0)