!7 !include once #1026298 // DynTalkBot2 for JDA 4.0 set flag AllowMetaCode. meta-transformNow { tok_cmdStatement } standardBot1 RolesExporter { allServers { transient S discordInvite = "https://discord.gg/RXqf97d"; transient S status = discordInvite; } init { print("All permissions: " + Permission.ALL_PERMISSIONS); if (myName == null) setField(myName := "HyperCubes"); preprocessAtSelfToMyName = true; escapeAtEveryone = true; onDiscordStarted(r { doEveryAndNow(20.0, r { gazelle_discord_playingGame(status) }); }); } sync S processSimplifiedLine(S s, O... _) null { try answer super.processSimplifiedLine(s, _); optPar long channelID; if (channelID == 0) null; optPar Message msg; Message.Attachment attachment = msg == null ? null : first(msg.getAttachments()); if (attachment != null) { File file = appendToFileName(downloadFileForChannel(channelID), "." + now()); print("Downloading attachment " + attachment.getFileName() + " to " + file); bool authed = checkPerms(msg) == null; deleteFile(file); attachment.downloadToFile(file) .exceptionally(error -> { temp enter(); print("download fail"); ret null with postInChannel(channelID, "Couldn't download attachment"); }) .thenAccept(file2 -> postInChannel(channelID, handleAttachment(file2, channelID, authed))); } if null (s = dropMyPrefixOrNull(s)) null; optPar MessageChannel channel; try { cmd "help" { new EmbedBuilder eb; eb.setColor(toColor("F14527")); // "? ? ? ? ? ? ? ? ? ?" eb.setAuthor(hexToUTF8("e2848d20e284bd20e2849920e284b020e2849b20e2848220e182ae20e284ac20e284b020d58f"), null, null); eb.setTitle("Backup Your Discord Roles", null); eb.setDescription([[ In next update: Manage roles, full layout + roles backup (This bot is still under development, many features will be added later on.) If you like to support this project, join us at and consider donating. We thank you in advance If you are interested to make your own smart bot, visit ]]); discordEmbed_addField(eb, "Upload", "Upload a JSON file to channel to inspect/import it"); S syntax = [[ Export roles + members as JSON ` HyperCubes export [e]` Check bot's rights ` HyperCubes rights [r]` Diff last upload against server ` HyperCubes compare [c]` Add missing roles from last upload ` HyperCubes add roles [ar]` List roles that members should have according to last import ` HyperCubes list missing roles [lmr]` Add all roles mentioned in last line ` HyperCubes add member roles [amr]` List roles not used in current server ` HyperCubes unused roles [ur]` Delete unused roles ` HyperCubes delete unused roles [dur]` ]]; discordEmbed_addField(eb, "Syntax (abbreviations in brackets)", syntax); eb.setThumbnail(imageSnippetURL_noHttps(#1102865)); channel.sendMessage(eb.build()).queue(); null; } cmd "old help" ret trimAllLines([[ Upload a JSON file to channel to inspect/import it @myName export - export roles + members as JSON @myName rights - check bot's rights @myName diff - diff last upload against server MILDLY DANGEROUS @myName add roles - add missing roles from last upload @myName unused roles - list roles not used in current server DANGEROUS @myName delete unused roles - delete unused roles @myName missing member roles - list roles that members should have according to last import SOMEWHAT DANGEROUS @myName add member roles - add all roles mentioned in last line ]].replace("@myName", myName); cmd "export|e" ret uploadFileInChannel(channelID, toUtf8(jsonEncode_breakAtNLevels(3, makeData())), "members-and-roles-" + guild.getIdLong() + ".json.txt", null, null); cmd "rights|r" ret canManageRoles() ? "I have the right to manage roles. I have everything I want!" : "I am not authorized to manage roles, so I can only preview changes, not perform them."; cmd "compare|c" { File f = downloadFileForChannel(channelID); if (!fileExists(f)) ret "No past upload found in this channel"; ret pairA(diff(f)); } cmd "add roles|ar" ret or2(addRoles(_), "Nothing to do"); cmd "unused roles|ur" ret or2(lines(unusedRoles()), "No unused roles"); cmd "delete unused roles|dur" { try answer checkPerms(msg); ret or2(mapToLines(unusedRoles(), role -> { queue(role.delete()); ret "Deleted role: " + role.getName(); }), "No unused roles"); } cmd "list missing roles|lmr" ret or2(missingMemberRoles(false, _), "Nothing to do"); cmd "add member roles|amr" { try answer checkPerms(msg); S a = addRoles(_); ret or2(a + missingMemberRoles(true, _), "Nothing to do"); } cmd "stats" ret "Brains: " + l(dataByServer) + "\n" + "Persistent data size: " + (l(dm_moduleStruct()) + l(conceptsFile())); cmd "invite" ret "Invite me to a server: <" + discordBotInviteLink(discordBotID, discord_adminPermission()) + ">\n" + "Join my Discord: <" + discordInvite + ">"; } catch print e { ret "Internal error: " + e; } } S missingMemberRoles(bool doIt, O... _) { optPar long channelID; Either p = diffWithLastUpload(channelID); if (!isB(p)) ret eitherGetA(p); Map diff = cast p.b(); Map membersDiff = cast mapGet(diff, "members"); new LS buf; for (JsonDiff.GatheredKey memberID, O memberDiff : unnull(membersDiff)) { long _memberID = (long) memberID!; Member member = guild.getMemberById(_memberID); if (member == null) continue; //buf.add(roleName + ": " + className(roleDiff)); if (memberDiff cast Map) { Cl roleDiffs = values((Map) get roles(memberDiff)); for (O roleDiff : roleDiffs) if (roleDiff cast JsonDiff.Added) { S roleName = getString name(roleDiff.b); buf.add(discordAt(_memberID) + " needs role " + roleName); if (doIt) { L roles = guild.getRolesByName(roleName, false); if (empty(roles)) buf.add("Role " + quote(roleName) + " not found"); for (Role role : roles) { queue(guild.addRoleToMember(member, role)); buf.add(" Added role " + role); } } } } } ret lines(buf); } L unusedRoles() { ret filter(guild.getRoles(), r -> empty(guild.getMembersWithRoles(r))); } S checkPerms(Message msg) { if (!msg.getMember().hasPermission(Permission.MANAGE_ROLES)) ret "You have no permission to do that you sneaky boy"; null; } File downloadFileForChannel(long channelID) { ret prepareCacheProgramFile(guildID + "/" + channelID); } bool canManageRoles() { ret guild.getSelfMember().hasPermission(Permission.MANAGE_ROLES); } Map makeData() { L roles = map(guild.getRoles(), r -> litorderedmap( "name" := r.getName(), "id" := r.getIdLong(), "permissions" := r.getPermissionsRaw())); L members = map(guild.getMembers(), m -> litorderedmap( "nickName" := m.getNickname(), "effectiveName" := m.getEffectiveName(), "userName" := m.getUser().getName(), "id" := m.getIdLong(), "isOwner" := trueOrNull(m.isOwner()), "roles" := map(m.getRoles(), r -> litorderedmap("name" := r.getName(), "id" := r.getIdLong())))); ret litorderedmap( dataType := "members and roles for guild", dateExported := dateWithSecondsGMT(), guild := litorderedmap( name := guild.getName(), id := guild.getIdLong()), +roles, +members); } S handleAttachment(File file, long channelID, bool authed) enter { try { print("Handling attachment " + f2s(file)); O json; try { json = decodeJson(loadTextFile(file)); } catch e { ret null with print("Not a JSON file: " + f2s(file)); } if (!isMyKindOfData(json)) null; S a = ""; if (authed) { copyFile(file, downloadFileForChannel(channelID)); a = "Saving upload\n"; } ret a + unnull(pairA(diff(file))); } catch print e { ret "Internal error"; } } // returns null if error // else: result for user, diff Pair diff(File file) { O json; try { json = decodeJson(loadTextFile(file)); } catch e { ret null with print("Not a JSON file: " + f2s(file)); } fixImportedData(json); new JsonDiff differ; // identify roles by names and members by ID differ.getListKey = path -> eq(last(path), 'roles) ? 'name : 'id; O diff = differ.diff(makeData(), json); //ret "Differences: " + structForUser(diff); ret pair("Differences:\n" + nicelyFormatJsonDiff(diff), diff); } void fixImportedData(O data) { changeKey((Map) data, 'allRoles, 'roles); } bool isMyKindOfData(O data) { ret isMap(data) && containsOneKeyOf((Map) data, "roles", "allRoles"); } // either error or diff Either diffWithLastUpload(long channelID) { File f = downloadFileForChannel(channelID); if (!fileExists(f)) ret eitherA("No past upload found in this channel"); Pair p = diff(f); if (p == null) ret eitherA("There was an error with the last upload"); ret eitherB(p.b); } S addRoles(O... _) { optPar Message msg; optPar long channelID; try answer checkPerms(msg); Either p = diffWithLastUpload(channelID); if (!isB(p)) ret eitherGetA(p); Map diff = cast p.b(); Map rolesDiff = cast mapGet(diff, "roles"); print(+rolesDiff); new LS buf; for (JsonDiff.GatheredKey roleName, O roleDiff : unnull(rolesDiff)) { //buf.add(roleName + ": " + className(roleDiff)); if (roleDiff cast JsonDiff.Added) { //buf.add("Add role " + quote(roleName) + ": " + roleDiff); //buf.add(className(roleDiff.b)); Map roleMap = cast roleDiff.b; print(+roleMap); S name = getString name(roleMap); long permissions = getLong permissions(roleMap); guild.createRole() .setName(name) .setPermissions(permissions) .complete(); // further actions may depend on this, so block until done buf.add("Added role " + quote(name)); } } ret lines(buf); } }