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