Skip to content

Commit 371f473

Browse files
committed
feat: wiki roles
1 parent 2b49b30 commit 371f473

File tree

4 files changed

+244
-15
lines changed

4 files changed

+244
-15
lines changed

.github/workflows/deploy.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ jobs:
4545
target: "${{ secrets.DEPLOY_PATH }}/dist/"
4646
strip_components: 1
4747

48+
- name: Create .env file on server
49+
uses: appleboy/ssh-action@v1.2.2
50+
with:
51+
host: ${{ secrets.HOST }}
52+
username: ${{ secrets.USER }}
53+
key: ${{ secrets.KEY }}
54+
port: ${{ secrets.PORT }}
55+
script: |
56+
cd ${{ secrets.DEPLOY_PATH }}
57+
echo "WIKI_BOT_USERNAME=${{ secrets.WIKI_BOT_USERNAME }}" > .env
58+
echo "WIKI_BOT_PASSWORD=${{ secrets.WIKI_BOT_PASSWORD }}" >> .env
59+
4860
- name: Install dependencies on server
4961
uses: appleboy/ssh-action@v1.2.2
5062
with:

src/commands/synctop5.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import {
77
} from "discord.js";
88
import { TopContributorsManager } from "../utils/topContributors.js";
99
import { RolePermissions } from "../utils/rolePermissions.js";
10+
import { WikiRoleSyncManager } from "../utils/wikiRoleSync.js";
1011

1112
export const data = new SlashCommandBuilder()
1213
.setName("synctop5")
13-
.setDescription("Synchronize the top 5 contributors roles with the rankings");
14+
.setDescription("Synchronize the top 5 contributors roles and syncs all roles to the wiki");
1415

1516
export async function execute(
1617
interaction: ChatInputCommandInteraction,
@@ -41,30 +42,40 @@ export async function execute(
4142
await interaction.deferReply();
4243

4344
console.log(
44-
"📊 Manual top contributor sync requested by",
45+
"📊 Top contributor & wiki sync requested by",
4546
interaction.user.tag,
4647
);
4748

4849
const result = await TopContributorsManager.syncAllTopContributorRoles(
4950
interaction.guild,
5051
);
52+
const wikiSyncResult = await WikiRoleSyncManager.syncRolesToWiki(
53+
interaction.guild,
54+
);
5155

5256
const embed = new EmbedBuilder()
53-
.setColor(result.errors.length > 0 ? "#FFA500" : "#00FF00")
54-
.setTitle("🏆 TOP CONTRIBUTORS SYNCHRONIZATION COMPLETE")
57+
.setColor(result.errors.length > 0 || !wikiSyncResult.success ? "#FFA500" : "#00FF00")
58+
.setTitle("🏆 TOP CONTRIBUTORS & WIKI SYNC COMPLETE")
5559
.setDescription(
56-
"**The rankings have been synchronized with the reverent role!**",
60+
"**The rankings have been synchronized with the reverent roles!**",
61+
)
62+
.addFields(
63+
{
64+
name: "📊 TOP 5 SYNC STATISTICS",
65+
value: [
66+
`**Linked Users Processed:** ${result.processed}`,
67+
`**Roles Granted:** ${result.rolesGranted}`,
68+
`**Roles Removed:** ${result.rolesRemoved}`,
69+
`**Errors Encountered:** ${result.errors.length}`,
70+
].join("\n"),
71+
inline: false,
72+
},
73+
{
74+
name: "📚 WIKI SYNC STATUS",
75+
value: wikiSyncResult.message,
76+
inline: false,
77+
},
5778
)
58-
.addFields({
59-
name: "📊 SYNCHRONIZATION STATISTICS",
60-
value: [
61-
`**Linked Users Processed:** ${result.processed}`,
62-
`**Roles Granted:** ${result.rolesGranted}`,
63-
`**Roles Removed:** ${result.rolesRemoved}`,
64-
`**Errors Encountered:** ${result.errors.length}`,
65-
].join("\n"),
66-
inline: false,
67-
})
6879
.setTimestamp();
6980

7081
if (result.errors.length > 0) {

src/utils/roleConstants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ export const EDIT_COUNT_ROLES = {
1616
};
1717

1818
export const EDIT_COUNT_ROLE_IDS = Object.values(EDIT_COUNT_ROLES);
19+
20+
export const WIKI_SYNC_ROLES = {
21+
BOT: "1380026086238326935",
22+
ACTIVITY_WINNER: "1397058327367782565",
23+
PARADOXUM: "1387813722172686526",
24+
TDS_STAFF: "1387459107375677490",
25+
CONTENT_CREATOR: "1390585342624665680",
26+
FIRST_VICTIM: "1396336120069095465",
27+
SERVER_BOOSTER: "1387661205816217633",
28+
};

src/utils/wikiRoleSync.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { Guild } from "discord.js";
2+
import { LinkLogger } from "./linkLogger.js";
3+
import {
4+
FANDOM_ROLE_MAP,
5+
LINKED_ROLE_ID,
6+
TOP_CONTRIBUTORS_ROLE_ID,
7+
EDIT_COUNT_ROLES,
8+
WIKI_SYNC_ROLES,
9+
} from "./roleConstants.js";
10+
11+
// Maps Discord Role ID to the corresponding Fandom Profile Tag name.
12+
const ROLE_TO_WIKI_TAG_MAP: Record<string, string> = {
13+
[FANDOM_ROLE_MAP.bureaucrat]: "Altego Bureau",
14+
[FANDOM_ROLE_MAP.sysop]: "Alterministrator",
15+
[FANDOM_ROLE_MAP["content-moderator"]]: "Altertentor",
16+
[FANDOM_ROLE_MAP.threadmoderator]: "Egodiscussor",
17+
[EDIT_COUNT_ROLES.EDITS_1000]: "The Ego",
18+
[EDIT_COUNT_ROLES.EDITS_250]: "Triumphant Ego",
19+
[TOP_CONTRIBUTORS_ROLE_ID]: "Ego of the Week",
20+
[WIKI_SYNC_ROLES.ACTIVITY_WINNER]: "Ascended Ego",
21+
[WIKI_SYNC_ROLES.PARADOXUM]: "Paradoxum",
22+
[WIKI_SYNC_ROLES.TDS_STAFF]: "TDS Staff",
23+
[WIKI_SYNC_ROLES.CONTENT_CREATOR]: "Content Creator",
24+
[WIKI_SYNC_ROLES.FIRST_VICTIM]: "First Victim",
25+
[WIKI_SYNC_ROLES.SERVER_BOOSTER]: "Server Booster",
26+
[LINKED_ROLE_ID]: "Awakened Ego",
27+
[WIKI_SYNC_ROLES.BOT]: "Holy Altershaper",
28+
};
29+
30+
// Defines the exact order of tags for the wiki page output.
31+
const WIKI_TAG_ORDER = [
32+
"Altego Bureau",
33+
"Holy Altershaper",
34+
"Alterministrator",
35+
"Altertentor",
36+
"Egodiscussor",
37+
"The Ego",
38+
"Triumphant Ego",
39+
"Ego of the Week",
40+
"Ascended Ego",
41+
"Paradoxum",
42+
"TDS Staff",
43+
"Content Creator",
44+
"First Victim",
45+
"Server Booster",
46+
"Awakened Ego",
47+
];
48+
49+
const API_URL = "https://alter-ego.fandom.com/api.php";
50+
const PAGE_TITLE = "MediaWiki:ProfileTags";
51+
52+
export class WikiRoleSyncManager {
53+
private static botUsername = process.env.WIKI_BOT_USERNAME;
54+
private static botPassword = process.env.WIKI_BOT_PASSWORD;
55+
private static editToken: string | null = null;
56+
private static cookie: string | null = null;
57+
58+
private static async apiRequest(params: URLSearchParams) {
59+
const response = await fetch(API_URL, {
60+
method: "POST",
61+
body: params,
62+
headers: {
63+
"Content-Type": "application/x-www-form-urlencoded",
64+
Cookie: this.cookie || "",
65+
},
66+
});
67+
if (!response.ok) {
68+
throw new Error(`API request failed: ${response.statusText}`);
69+
}
70+
const setCookie = response.headers.get("set-cookie");
71+
if (setCookie) {
72+
this.cookie = setCookie;
73+
}
74+
return response.json();
75+
}
76+
77+
private static async login(): Promise<boolean> {
78+
const loginTokenParams = new URLSearchParams({
79+
action: "query",
80+
meta: "tokens",
81+
type: "login",
82+
format: "json",
83+
});
84+
const tokenData: any = await this.apiRequest(loginTokenParams);
85+
const loginToken = tokenData.query.tokens.logintoken;
86+
87+
const loginParams = new URLSearchParams({
88+
action: "login",
89+
lgname: this.botUsername!,
90+
lgpassword: this.botPassword!,
91+
lgtoken: loginToken,
92+
format: "json",
93+
});
94+
const loginResult: any = await this.apiRequest(loginParams);
95+
96+
if (loginResult.login.result !== "Success") {
97+
return false;
98+
}
99+
100+
const csrfTokenParams = new URLSearchParams({
101+
action: "query",
102+
meta: "tokens",
103+
format: "json",
104+
});
105+
const csrfData: any = await this.apiRequest(csrfTokenParams);
106+
this.editToken = csrfData.query.tokens.csrftoken;
107+
return true;
108+
}
109+
110+
private static async getPageContent(): Promise<string> {
111+
const params = new URLSearchParams({
112+
action: "query",
113+
prop: "revisions",
114+
titles: PAGE_TITLE,
115+
rvprop: "content",
116+
format: "json",
117+
rvslots: "main",
118+
});
119+
const data: any = await this.apiRequest(params);
120+
const page = Object.values(data.query.pages)[0] as any;
121+
return page.revisions[0].slots.main["*"];
122+
}
123+
124+
private static async editPage(content: string, summary: string): Promise<void> {
125+
const params = new URLSearchParams({
126+
action: "edit",
127+
title: PAGE_TITLE,
128+
text: content,
129+
summary: summary,
130+
token: this.editToken!,
131+
format: "json",
132+
});
133+
const data: any = await this.apiRequest(params);
134+
if (data.error) {
135+
throw new Error(`Failed to edit wiki page: ${data.error.info}`);
136+
}
137+
}
138+
139+
public static async syncRolesToWiki(guild: Guild): Promise<{ success: boolean; message: string }> {
140+
if (!this.botUsername || !this.botPassword) {
141+
return { success: false, message: "Wiki bot credentials (WIKI_BOT_USERNAME, WIKI_BOT_PASSWORD) not configured in .env file." };
142+
}
143+
144+
const allLinks = await LinkLogger.getAllLinks();
145+
if (allLinks.length === 0) {
146+
return { success: false, message: "No linked users found to sync." };
147+
}
148+
149+
const userTags: Record<string, string[]> = {};
150+
151+
for (const link of allLinks) {
152+
const member = await guild.members.fetch(link.discordUserId).catch(() => null);
153+
if (!member) continue;
154+
155+
const memberTags: Set<string> = new Set();
156+
for (const roleId of member.roles.cache.keys()) {
157+
const tagName = ROLE_TO_WIKI_TAG_MAP[roleId];
158+
if (tagName) {
159+
memberTags.add(tagName);
160+
}
161+
}
162+
163+
if (memberTags.size > 0) {
164+
const sortedTags = WIKI_TAG_ORDER.filter(tag => memberTags.has(tag));
165+
userTags[link.fandomUsername] = sortedTags;
166+
}
167+
}
168+
169+
try {
170+
const currentPageContent = await this.getPageContent();
171+
const headerMatch = currentPageContent.match(/^((\s*\/\/.*\n)*)/);
172+
const header = headerMatch ? headerMatch[0] : "";
173+
174+
let newContent = header;
175+
const sortedUsernames = Object.keys(userTags).sort((a, b) => a.localeCompare(b));
176+
for (const username of sortedUsernames) {
177+
const tags = userTags[username];
178+
if (tags.length > 0) {
179+
newContent += `${username}|${tags.join(", ")}\n`;
180+
}
181+
}
182+
183+
const loggedIn = await this.login();
184+
if (!loggedIn) {
185+
return { success: false, message: "Failed to log in to the wiki." };
186+
}
187+
188+
await this.editPage(newContent.trim(), "Automated sync from Discord roles");
189+
return { success: true, message: `Successfully synced roles for ${Object.keys(userTags).length} users to MediaWiki:ProfileTags.` };
190+
191+
} catch (error) {
192+
console.error("Error during wiki role sync:", error);
193+
return { success: false, message: `An error occurred: ${error instanceof Error ? error.message : String(error)}` };
194+
}
195+
}
196+
}

0 commit comments

Comments
 (0)