This repository has been archived by the owner on Sep 12, 2021. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 66
/
discord.dm
276 lines (245 loc) · 10.6 KB
/
discord.dm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/**
* # Discord Subsystem
*
* This subsystem handles some integrations with discord
*
*
* NOTES:
* * There is a DB table to track ckeys and associated discord IDs. (discord_link)
* * This system REQUIRES TGS for notifying users at end of the round
* * The SS uses fire() instead of just pure shutdown, so people can be notified if it comes back after a crash, where the SS wasn't properly shutdown
* * It only writes to the disk every 5 minutes, and it won't write to disk if the file is the same as it was the last time it was written. This is to save on disk writes
* * The system is kept per-server (EG: Terry will not notify people who pressed notify on Sybil), but the accounts are between servers so you dont have to relink on each server.
*
*
* ## HOW NOTIFYING WORKS
*
* ### ROUNDSTART:
* 1) The file is loaded and the discord IDs are extracted
* 2) A ping is sent to the discord with the IDs of people who wished to be notified
* 3) The file is emptied
*
* ### MIDROUND:
* 1) Someone usees the notify verb, it adds their discord ID to the list.
* 2) On fire, it will write that to the disk, as long as conditions above are correct
*
* ### END ROUND:
* 1) The file is force-saved, incase it hasn't fired at end round
*
* This is an absolute clusterfuck, but its my clusterfuck -aa07
*/
SUBSYSTEM_DEF(discord)
name = "Discord"
wait = 3000
init_order = INIT_ORDER_DISCORD
/// People to save to notify file
var/list/notify_members = list()
/// Copy of previous list, so the SS doesnt have to fire if no new members have been added
var/list/notify_members_cache = list()
/// People to notify on roundstart
var/list/people_to_notify = list()
/// People who have tried to verify this round already
var/list/reverify_cache
/// Common words list, used to generate one time tokens
var/list/common_words
/// The file where notification status is saved
var/notify_file = file("data/notify.json")
/// Is TGS enabled (If not we won't fire because otherwise this is useless)
var/enabled = 0
/datum/controller/subsystem/discord/Initialize(start_timeofday)
common_words = world.file2list("strings/1000_most_common.txt")
reverify_cache = list()
// Check for if we are using TGS, otherwise return and disables firing
if(world.TgsAvailable())
enabled = 1 // Allows other procs to use this (Account linking, etc)
else
can_fire = 0 // We dont want excess firing
return ..() // Cancel
try
people_to_notify = json_decode(file2text(notify_file))
catch
pass() // The list can just stay as its default (blank). Pass() exists because it needs a catch
var/notifymsg = jointext(people_to_notify, ", ")
if(notifymsg)
notifymsg += ", a new round is starting!"
send2chat(trim(notifymsg), CONFIG_GET(string/chat_announce_new_game)) // Sends the message to the discord, using same config option as the roundstart notification
fdel(notify_file) // Deletes the file
return ..()
/datum/controller/subsystem/discord/fire()
if(!enabled)
return // Dont do shit if its disabled
if(notify_members == notify_members_cache)
return // Dont re-write the file
// If we are all clear
write_notify_file()
/datum/controller/subsystem/discord/Shutdown()
write_notify_file() // Guaranteed force-write on server close
/datum/controller/subsystem/discord/proc/write_notify_file()
if(!enabled) // Dont do shit if its disabled
return
fdel(notify_file) // Deletes the file first to make sure it writes properly
WRITE_FILE(notify_file, json_encode(notify_members)) // Writes the file
notify_members_cache = notify_members // Updates the cache list
/**
* Given a ckey, look up the discord user id attached to the user, if any
*
* This gets the most recent entry from the discord link table that is associated with the given ckey
*
* Arguments:
* * lookup_ckey A string representing the ckey to search on
*/
/datum/controller/subsystem/discord/proc/lookup_id(lookup_ckey)
var/datum/discord_link_record/link = find_discord_link_by_ckey(lookup_ckey)
if(link)
return link.discord_id
/**
* Given a discord id as a string, look up the ckey attached to that account, if any
*
* This gets the most recent entry from the discord_link table that is associated with this discord id snowflake
*
* Arguments:
* * lookup_id The discord id as a string
*/
/datum/controller/subsystem/discord/proc/lookup_ckey(lookup_id)
var/datum/discord_link_record/link = find_discord_link_by_discord_id(lookup_id)
if(link)
return link.ckey
/datum/controller/subsystem/discord/proc/get_or_generate_one_time_token_for_ckey(ckey)
// Is there an existing valid one time token
var/datum/discord_link_record/link = find_discord_link_by_ckey(ckey, timebound = TRUE)
if(link)
return link.one_time_token
// Otherwise we make one
return generate_one_time_token(ckey)
/**
* Generate a timebound token for discord verification
*
* This uses the common word list to generate a six word random token, this token can then be fed to a discord bot that has access
* to the same database, and it can use it to link a ckey to a discord id, with minimal user effort
*
* It returns the token to the calling proc, after inserting an entry into the discord_link table of the following form
*
* ```
* (unique_id, ckey, null, the current time, the one time token generated)
* the null value will be filled out with the discord id by the integrated discord bot when a user verifies
* ```
*
* Notes:
* * The token is guaranteed to unique during it's validity period
* * The validity period is currently set at 4 hours
* * a token may not be unique outside it's validity window (to reduce conflicts)
*
* Arguments:
* * ckey_for a string representing the ckey this token is for
*
* Returns a string representing the one time token
*/
/datum/controller/subsystem/discord/proc/generate_one_time_token(ckey_for)
var/not_unique = TRUE
var/one_time_token = ""
// While there's a collision in the token, generate a new one (should rarely happen)
while(not_unique)
//Column is varchar 100, so we trim just in case someone does us the dirty later
one_time_token = trim("[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]", 100)
not_unique = find_discord_link_by_token(one_time_token, timebound = TRUE)
// Insert into the table, null in the discord id, id and timestamp and valid fields so the db fills them out where needed
var/datum/DBQuery/query_insert_link_record = SSdbcore.NewQuery(
"INSERT INTO [format_table_name("discord_links")] (ckey, one_time_token) VALUES(:ckey, :token)",
list("ckey" = ckey_for, "token" = one_time_token)
)
if(!query_insert_link_record.Execute())
qdel(query_insert_link_record)
return ""
//Cleanup
qdel(query_insert_link_record)
return one_time_token
/**
* Find discord link entry by the passed in user token
*
* This will look into the discord link table and return the *first* entry that matches the given one time token
*
* Remember, multiple entries can exist, as they are only guaranteed to be unique for their validity period
*
* Arguments:
* * one_time_token the string of words representing the one time token
* * timebound A boolean flag, that specifies if it should only look for entries within the last 4 hours, off by default
*
* Returns a [/datum/discord_link_record]
*/
/datum/controller/subsystem/discord/proc/find_discord_link_by_token(one_time_token, timebound = FALSE)
var/timeboundsql = ""
if(timebound)
timeboundsql = "AND timestamp >= Now() - INTERVAL 4 HOUR"
var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE one_time_token = :one_time_token [timeboundsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1"
var/datum/DBQuery/query_get_discord_link_record = SSdbcore.NewQuery(
query,
list("one_time_token" = one_time_token)
)
if(!query_get_discord_link_record.Execute())
qdel(query_get_discord_link_record)
return
if(query_get_discord_link_record.NextRow())
var/result = query_get_discord_link_record.item
. = new /datum/discord_link_record(result[2], result[1], result[4], result[3])
//Make sure we clean up the query
qdel(query_get_discord_link_record)
/**
* Find discord link entry by the passed in user ckey
*
* This will look into the discord link table and return the *first* entry that matches the given ckey
*
* Remember, multiple entries can exist
*
* Arguments:
* * ckey the users ckey as a string
* * timebound should we search only in the last 4 hours
*
* Returns a [/datum/discord_link_record]
*/
/datum/controller/subsystem/discord/proc/find_discord_link_by_ckey(ckey, timebound = FALSE)
var/timeboundsql = ""
if(timebound)
timeboundsql = "AND timestamp >= Now() - INTERVAL 4 HOUR"
var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE ckey = :ckey [timeboundsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1"
var/datum/DBQuery/query_get_discord_link_record = SSdbcore.NewQuery(
query,
list("ckey" = ckey)
)
if(!query_get_discord_link_record.Execute())
qdel(query_get_discord_link_record)
return
if(query_get_discord_link_record.NextRow())
var/result = query_get_discord_link_record.item
. = new /datum/discord_link_record(result[2], result[1], result[4], result[3])
//Make sure we clean up the query
qdel(query_get_discord_link_record)
/**
* Find discord link entry by the passed in user ckey
*
* This will look into the discord link table and return the *first* entry that matches the given ckey
*
* Remember, multiple entries can exist
*
* Arguments:
* * discord_id The users discord id (string)
* * timebound should we search only in the last 4 hours
*
* Returns a [/datum/discord_link_record]
*/
/datum/controller/subsystem/discord/proc/find_discord_link_by_discord_id(discord_id, timebound = FALSE)
var/timeboundsql = ""
if(timebound)
timeboundsql = "AND timestamp >= Now() - INTERVAL 4 HOUR"
var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE discord_id = :discord_id [timeboundsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1"
var/datum/DBQuery/query_get_discord_link_record = SSdbcore.NewQuery(
query,
list("discord_id" = discord_id)
)
if(!query_get_discord_link_record.Execute())
qdel(query_get_discord_link_record)
return
if(query_get_discord_link_record.NextRow())
var/result = query_get_discord_link_record.item
. = new /datum/discord_link_record(result[2], result[1], result[4], result[3])
//Make sure we clean up the query
qdel(query_get_discord_link_record)