mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-22 05:54:36 +00:00
Remove huge lock on cache
This commit is contained in:
parent
92f4c42b5b
commit
0c4d39e24d
9 changed files with 227 additions and 234 deletions
|
@ -40,7 +40,7 @@ Class BackgroundServiceRunner
|
||||||
' Delay a bit before we start (or continue) work.
|
' Delay a bit before we start (or continue) work.
|
||||||
Await Task.Delay(Interval * 1000, WorkerCancel.Token)
|
Await Task.Delay(Interval * 1000, WorkerCancel.Token)
|
||||||
|
|
||||||
' Start background tasks.
|
' Execute background tasks.
|
||||||
Dim tasks As New List(Of Task)
|
Dim tasks As New List(Of Task)
|
||||||
For Each service In Workers
|
For Each service In Workers
|
||||||
tasks.Add(service.OnTick(_tickCount))
|
tasks.Add(service.OnTick(_tickCount))
|
||||||
|
|
|
@ -58,23 +58,25 @@ Class BirthdayRoleUpdate
|
||||||
Dim channel As SocketTextChannel = Nothing
|
Dim channel As SocketTextChannel = Nothing
|
||||||
Dim announce As (String, String)
|
Dim announce As (String, String)
|
||||||
Dim announceping As Boolean
|
Dim announceping As Boolean
|
||||||
SyncLock BotInstance.KnownGuilds
|
|
||||||
If Not BotInstance.KnownGuilds.ContainsKey(guild.Id) Then Return 0
|
|
||||||
Dim gs = BotInstance.KnownGuilds(guild.Id)
|
|
||||||
tz = gs.TimeZone
|
|
||||||
users = gs.Users
|
|
||||||
announce = gs.AnnounceMessages
|
|
||||||
announceping = gs.AnnouncePing
|
|
||||||
|
|
||||||
If gs.AnnounceChannelId.HasValue Then channel = guild.GetTextChannel(gs.AnnounceChannelId.Value)
|
If Not BotInstance.GuildCache.ContainsKey(guild.Id) Then Return 0 ' guild not yet fully loaded; skip processing
|
||||||
If gs.RoleId.HasValue Then role = guild.GetRole(gs.RoleId.Value)
|
|
||||||
|
Dim gs = BotInstance.GuildCache(guild.Id)
|
||||||
|
With gs
|
||||||
|
tz = .TimeZone
|
||||||
|
users = .Users
|
||||||
|
announce = .AnnounceMessages
|
||||||
|
announceping = .AnnouncePing
|
||||||
|
|
||||||
|
If .AnnounceChannelId.HasValue Then channel = guild.GetTextChannel(gs.AnnounceChannelId.Value)
|
||||||
|
If .RoleId.HasValue Then role = guild.GetRole(gs.RoleId.Value)
|
||||||
If role Is Nothing Then
|
If role Is Nothing Then
|
||||||
gs.RoleWarningNonexist = True
|
.RoleWarningNonexist = True
|
||||||
Return 0
|
Return 0
|
||||||
Else
|
Else
|
||||||
gs.RoleWarningNonexist = False
|
.RoleWarningNonexist = False
|
||||||
End If
|
End If
|
||||||
End SyncLock
|
End With
|
||||||
|
|
||||||
' Determine who's currently having a birthday
|
' Determine who's currently having a birthday
|
||||||
Dim birthdays = GetGuildCurrentBirthdays(users, tz)
|
Dim birthdays = GetGuildCurrentBirthdays(users, tz)
|
||||||
|
@ -100,9 +102,7 @@ Class BirthdayRoleUpdate
|
||||||
|
|
||||||
' Update warning flag
|
' Update warning flag
|
||||||
Dim updateError = Not correctRolePermissions Or gotForbidden
|
Dim updateError = Not correctRolePermissions Or gotForbidden
|
||||||
SyncLock BotInstance.KnownGuilds
|
BotInstance.GuildCache(guild.Id).RoleWarningPermission = updateError
|
||||||
BotInstance.KnownGuilds(guild.Id).RoleWarningPermission = updateError
|
|
||||||
End SyncLock
|
|
||||||
' Quit now if the warning flag was set. Announcement data is not available.
|
' Quit now if the warning flag was set. Announcement data is not available.
|
||||||
If updateError Then Return 0
|
If updateError Then Return 0
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ Class GuildStatistics
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Public Overrides Async Function OnTick(tick As Integer) As Task
|
Public Overrides Async Function OnTick(tick As Integer) As Task
|
||||||
' Activate roughly every 5 hours (interval: 45)
|
' Activate roughly every 2 hours (interval: 45)
|
||||||
If tick Mod 400 <> 2 Then Return
|
If tick Mod 160 <> 2 Then Return
|
||||||
|
|
||||||
Dim count = BotInstance.DiscordClient.Guilds.Count
|
Dim count = BotInstance.DiscordClient.Guilds.Count
|
||||||
Log($"Currently in {count} guild(s).")
|
Log($"Currently in {count} guild(s).")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
Imports BirthdayBot.CommandsCommon
|
Imports System.Collections.Concurrent
|
||||||
|
Imports BirthdayBot.CommandsCommon
|
||||||
Imports Discord
|
Imports Discord
|
||||||
Imports Discord.Net
|
Imports Discord.Net
|
||||||
Imports Discord.WebSocket
|
Imports Discord.WebSocket
|
||||||
|
@ -25,13 +26,12 @@ Class BirthdayBot
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
''' <summary>SyncLock when using. The lock object is itself.</summary>
|
Friend ReadOnly Property GuildCache As ConcurrentDictionary(Of ULong, GuildStateInformation)
|
||||||
Friend ReadOnly Property KnownGuilds As Dictionary(Of ULong, GuildStateInformation)
|
|
||||||
|
|
||||||
Public Sub New(conf As Configuration, dc As DiscordSocketClient)
|
Public Sub New(conf As Configuration, dc As DiscordSocketClient)
|
||||||
Config = conf
|
Config = conf
|
||||||
_client = dc
|
_client = dc
|
||||||
KnownGuilds = New Dictionary(Of ULong, GuildStateInformation)
|
GuildCache = New ConcurrentDictionary(Of ULong, GuildStateInformation)
|
||||||
|
|
||||||
_worker = New BackgroundServiceRunner(Me)
|
_worker = New BackgroundServiceRunner(Me)
|
||||||
|
|
||||||
|
@ -72,20 +72,16 @@ Class BirthdayBot
|
||||||
_client.Dispose()
|
_client.Dispose()
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Function LoadGuild(g As SocketGuild) As Task Handles _client.JoinedGuild, _client.GuildAvailable
|
Private Async Function LoadGuild(g As SocketGuild) As Task Handles _client.JoinedGuild, _client.GuildAvailable
|
||||||
SyncLock KnownGuilds
|
If Not GuildCache.ContainsKey(g.Id) Then
|
||||||
If Not KnownGuilds.ContainsKey(g.Id) Then
|
Dim gi = Await GuildStateInformation.LoadSettingsAsync(Config.DatabaseSettings, g.Id)
|
||||||
Dim gi = GuildStateInformation.LoadSettingsAsync(Config.DatabaseSettings, g.Id).GetAwaiter().GetResult()
|
GuildCache.TryAdd(g.Id, gi)
|
||||||
KnownGuilds.Add(g.Id, gi)
|
|
||||||
End If
|
End If
|
||||||
End SyncLock
|
|
||||||
Return Task.CompletedTask
|
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Function DiscardGuild(g As SocketGuild) As Task Handles _client.LeftGuild
|
Private Function DiscardGuild(g As SocketGuild) As Task Handles _client.LeftGuild
|
||||||
SyncLock KnownGuilds
|
Dim rm As GuildStateInformation = Nothing
|
||||||
KnownGuilds.Remove(g.Id)
|
GuildCache.TryRemove(g.Id, rm)
|
||||||
End SyncLock
|
|
||||||
Return Task.CompletedTask
|
Return Task.CompletedTask
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
|
@ -115,18 +111,14 @@ Class BirthdayBot
|
||||||
|
|
||||||
' Ban and role warning check
|
' Ban and role warning check
|
||||||
Dim roleWarningText As String
|
Dim roleWarningText As String
|
||||||
SyncLock KnownGuilds
|
Dim gi = GuildCache(channel.Guild.Id)
|
||||||
Dim gi = KnownGuilds(channel.Guild.Id)
|
|
||||||
|
|
||||||
' Skip ban check if user is a manager
|
' Skip ban check if user is a manager
|
||||||
If Not gi.IsUserModerator(author) Then
|
If Not gi.IsUserModerator(author) Then
|
||||||
If gi.IsUserBlockedAsync(author.Id).GetAwaiter().GetResult() Then
|
If gi.IsUserBlockedAsync(author.Id).GetAwaiter().GetResult() Then
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
End If
|
End If
|
||||||
|
|
||||||
roleWarningText = gi.IssueRoleWarning
|
roleWarningText = gi.IssueRoleWarning
|
||||||
End SyncLock
|
|
||||||
|
|
||||||
Try
|
Try
|
||||||
If roleWarningText IsNot Nothing Then
|
If roleWarningText IsNot Nothing Then
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<RootNamespace>BirthdayBot</RootNamespace>
|
<RootNamespace>BirthdayBot</RootNamespace>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||||
<Version>1.1.2</Version>
|
<Version>1.2.0</Version>
|
||||||
<Authors>Noiiko</Authors>
|
<Authors>Noiiko</Authors>
|
||||||
<Company />
|
<Company />
|
||||||
<Description>Discord bot for birthday reminders.</Description>
|
<Description>Discord bot for birthday reminders.</Description>
|
||||||
|
|
|
@ -31,6 +31,7 @@ Friend Class GuildStateInformation
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public ReadOnly Property IssueRoleWarning As String
|
Public ReadOnly Property IssueRoleWarning As String
|
||||||
Get
|
Get
|
||||||
|
SyncLock Me
|
||||||
If DateTimeOffset.UtcNow - _roleLastWarning > RoleWarningInterval Then
|
If DateTimeOffset.UtcNow - _roleLastWarning > RoleWarningInterval Then
|
||||||
_roleLastWarning = DateTimeOffset.UtcNow
|
_roleLastWarning = DateTimeOffset.UtcNow
|
||||||
Else
|
Else
|
||||||
|
@ -47,6 +48,7 @@ Friend Class GuildStateInformation
|
||||||
End If
|
End If
|
||||||
|
|
||||||
Return Nothing
|
Return Nothing
|
||||||
|
End SyncLock
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
@ -65,7 +67,9 @@ Friend Class GuildStateInformation
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Friend ReadOnly Property RoleWarningUnset As Boolean
|
Friend ReadOnly Property RoleWarningUnset As Boolean
|
||||||
Get
|
Get
|
||||||
|
SyncLock Me
|
||||||
Return _bdayRole Is Nothing
|
Return _bdayRole Is Nothing
|
||||||
|
End SyncLock
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
@ -75,9 +79,11 @@ Friend Class GuildStateInformation
|
||||||
Public ReadOnly Property Users As IEnumerable(Of GuildUserSettings)
|
Public ReadOnly Property Users As IEnumerable(Of GuildUserSettings)
|
||||||
Get
|
Get
|
||||||
Dim items As New List(Of GuildUserSettings)
|
Dim items As New List(Of GuildUserSettings)
|
||||||
|
SyncLock Me
|
||||||
For Each item In _userCache.Values
|
For Each item In _userCache.Values
|
||||||
items.Add(item)
|
items.Add(item)
|
||||||
Next
|
Next
|
||||||
|
End SyncLock
|
||||||
Return items
|
Return items
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
@ -87,7 +93,9 @@ Friend Class GuildStateInformation
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public ReadOnly Property RoleId As ULong?
|
Public ReadOnly Property RoleId As ULong?
|
||||||
Get
|
Get
|
||||||
|
SyncLock Me
|
||||||
Return _bdayRole
|
Return _bdayRole
|
||||||
|
End SyncLock
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
@ -96,7 +104,9 @@ Friend Class GuildStateInformation
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public ReadOnly Property AnnounceChannelId As ULong?
|
Public ReadOnly Property AnnounceChannelId As ULong?
|
||||||
Get
|
Get
|
||||||
|
SyncLock Me
|
||||||
Return _announceCh
|
Return _announceCh
|
||||||
|
End SyncLock
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
@ -105,7 +115,9 @@ Friend Class GuildStateInformation
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public ReadOnly Property TimeZone As String
|
Public ReadOnly Property TimeZone As String
|
||||||
Get
|
Get
|
||||||
|
SyncLock Me
|
||||||
Return _tz
|
Return _tz
|
||||||
|
End SyncLock
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
@ -114,7 +126,9 @@ Friend Class GuildStateInformation
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public ReadOnly Property IsModerated As Boolean
|
Public ReadOnly Property IsModerated As Boolean
|
||||||
Get
|
Get
|
||||||
|
SyncLock Me
|
||||||
Return _moderated
|
Return _moderated
|
||||||
|
End SyncLock
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
@ -123,7 +137,9 @@ Friend Class GuildStateInformation
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public ReadOnly Property ModeratorRole As ULong?
|
Public ReadOnly Property ModeratorRole As ULong?
|
||||||
Get
|
Get
|
||||||
|
SyncLock Me
|
||||||
Return _modRole
|
Return _modRole
|
||||||
|
End SyncLock
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
@ -132,17 +148,20 @@ Friend Class GuildStateInformation
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public ReadOnly Property AnnounceMessages As (String, String)
|
Public ReadOnly Property AnnounceMessages As (String, String)
|
||||||
Get
|
Get
|
||||||
|
SyncLock Me
|
||||||
Return (_announceMsg, _announceMsgPl)
|
Return (_announceMsg, _announceMsgPl)
|
||||||
|
End SyncLock
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
''' <summary>
|
''' <summary>
|
||||||
''' Gets whether to ping users in the announcement message instead of displaying their names.
|
''' Gets whether to ping users in the announcement message instead of displaying their names.
|
||||||
''' </summary>
|
''' </summary>
|
||||||
''' <returns></returns>
|
|
||||||
Public ReadOnly Property AnnouncePing As Boolean
|
Public ReadOnly Property AnnouncePing As Boolean
|
||||||
Get
|
Get
|
||||||
|
SyncLock Me
|
||||||
Return _announcePing
|
Return _announcePing
|
||||||
|
End SyncLock
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
@ -174,17 +193,22 @@ Friend Class GuildStateInformation
|
||||||
''' Gets user information from this guild. If the user doesn't exist in the backing database,
|
''' Gets user information from this guild. If the user doesn't exist in the backing database,
|
||||||
''' a new instance is created which is capable of adding the user to the database.
|
''' a new instance is created which is capable of adding the user to the database.
|
||||||
''' </summary>
|
''' </summary>
|
||||||
''' <param name="userId"></param>
|
''' <remarks>
|
||||||
|
''' For users with the Known property set to False, be sure to call
|
||||||
|
''' <see cref="GuildUserSettings.DeleteAsync(Database)"/> if the resulting object is otherwise unused.
|
||||||
|
''' </remarks>
|
||||||
Public Function GetUser(userId As ULong) As GuildUserSettings
|
Public Function GetUser(userId As ULong) As GuildUserSettings
|
||||||
|
SyncLock Me
|
||||||
If _userCache.ContainsKey(userId) Then
|
If _userCache.ContainsKey(userId) Then
|
||||||
Return _userCache(userId)
|
Return _userCache(userId)
|
||||||
End If
|
End If
|
||||||
|
|
||||||
' No result. Create a blank entry and add it to the list, in case it
|
' No result. Create a blank entry and add it to the list, in case it
|
||||||
' gets referenced later regardless of if having been updated or not.
|
' gets updated and then referenced later.
|
||||||
Dim blank As New GuildUserSettings(_GuildId, userId)
|
Dim blank As New GuildUserSettings(_GuildId, userId)
|
||||||
_userCache.Add(userId, blank)
|
_userCache.Add(userId, blank)
|
||||||
Return blank
|
Return blank
|
||||||
|
End SyncLock
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
''' <summary>
|
''' <summary>
|
||||||
|
@ -192,12 +216,13 @@ Friend Class GuildStateInformation
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public Async Function DeleteUserAsync(userId As ULong) As Task
|
Public Async Function DeleteUserAsync(userId As ULong) As Task
|
||||||
Dim user As GuildUserSettings = Nothing
|
Dim user As GuildUserSettings = Nothing
|
||||||
If _userCache.TryGetValue(userId, user) Then
|
SyncLock Me
|
||||||
Await user.DeleteAsync(_db)
|
If Not _userCache.TryGetValue(userId, user) Then
|
||||||
Else
|
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
_userCache.Remove(userId)
|
_userCache.Remove(userId)
|
||||||
|
End SyncLock
|
||||||
|
Await user.DeleteAsync(_db)
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
''' <summary>
|
''' <summary>
|
||||||
|
@ -208,6 +233,8 @@ Friend Class GuildStateInformation
|
||||||
Public Async Function IsUserBlockedAsync(userId As ULong) As Task(Of Boolean)
|
Public Async Function IsUserBlockedAsync(userId As ULong) As Task(Of Boolean)
|
||||||
If IsModerated Then Return True
|
If IsModerated Then Return True
|
||||||
|
|
||||||
|
' Block list is not cached, thus doing a database lookup
|
||||||
|
' TODO cache block list?
|
||||||
Using db = Await _db.OpenConnectionAsync()
|
Using db = Await _db.OpenConnectionAsync()
|
||||||
Using c = db.CreateCommand()
|
Using c = db.CreateCommand()
|
||||||
c.CommandText = $"select * from {BackingTableBans} " +
|
c.CommandText = $"select * from {BackingTableBans} " +
|
||||||
|
@ -229,9 +256,11 @@ Friend Class GuildStateInformation
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public Function IsUserModerator(user As SocketGuildUser) As Boolean
|
Public Function IsUserModerator(user As SocketGuildUser) As Boolean
|
||||||
If user.GuildPermissions.ManageGuild Then Return True
|
If user.GuildPermissions.ManageGuild Then Return True
|
||||||
|
SyncLock Me
|
||||||
If ModeratorRole.HasValue Then
|
If ModeratorRole.HasValue Then
|
||||||
If user.Roles.Where(Function(r) r.Id = ModeratorRole.Value).Count > 0 Then Return True
|
If user.Roles.Where(Function(r) r.Id = ModeratorRole.Value).Count > 0 Then Return True
|
||||||
End If
|
End If
|
||||||
|
End SyncLock
|
||||||
|
|
||||||
IsUserModerator = False
|
IsUserModerator = False
|
||||||
End Function
|
End Function
|
||||||
|
@ -240,6 +269,7 @@ Friend Class GuildStateInformation
|
||||||
''' Adds the specified user to the block list, preventing them from issuing commands.
|
''' Adds the specified user to the block list, preventing them from issuing commands.
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public Async Function BlockUserAsync(userId As ULong) As Task
|
Public Async Function BlockUserAsync(userId As ULong) As Task
|
||||||
|
' TODO cache block list?
|
||||||
Using db = Await _db.OpenConnectionAsync()
|
Using db = Await _db.OpenConnectionAsync()
|
||||||
Using c = db.CreateCommand()
|
Using c = db.CreateCommand()
|
||||||
c.CommandText = $"insert into {BackingTableBans} (guild_id, user_id) " +
|
c.CommandText = $"insert into {BackingTableBans} (guild_id, user_id) " +
|
||||||
|
@ -257,6 +287,7 @@ Friend Class GuildStateInformation
|
||||||
''' Removes the specified user from the block list.
|
''' Removes the specified user from the block list.
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Public Async Function UnbanUserAsync(userId As ULong) As Task
|
Public Async Function UnbanUserAsync(userId As ULong) As Task
|
||||||
|
' TODO cache block list?
|
||||||
Using db = Await _db.OpenConnectionAsync()
|
Using db = Await _db.OpenConnectionAsync()
|
||||||
Using c = db.CreateCommand()
|
Using c = db.CreateCommand()
|
||||||
c.CommandText = $"delete from {BackingTableBans} where " +
|
c.CommandText = $"delete from {BackingTableBans} where " +
|
||||||
|
@ -269,46 +300,60 @@ Friend Class GuildStateInformation
|
||||||
End Using
|
End Using
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Async Function UpdateRoleAsync(roleId As ULong) As Task
|
Public Sub UpdateRole(roleId As ULong)
|
||||||
|
SyncLock Me
|
||||||
_bdayRole = roleId
|
_bdayRole = roleId
|
||||||
_roleLastWarning = New DateTimeOffset
|
_roleLastWarning = New DateTimeOffset
|
||||||
Await UpdateDatabaseAsync()
|
UpdateDatabase()
|
||||||
End Function
|
End SyncLock
|
||||||
|
End Sub
|
||||||
|
|
||||||
Public Async Function UpdateAnnounceChannelAsync(channelId As ULong?) As Task
|
Public Sub UpdateAnnounceChannel(channelId As ULong?)
|
||||||
|
SyncLock Me
|
||||||
_announceCh = channelId
|
_announceCh = channelId
|
||||||
Await UpdateDatabaseAsync()
|
UpdateDatabase()
|
||||||
End Function
|
End SyncLock
|
||||||
|
End Sub
|
||||||
|
|
||||||
Public Async Function UpdateTimeZoneAsync(tzString As String) As Task
|
Public Sub UpdateTimeZone(tzString As String)
|
||||||
|
SyncLock Me
|
||||||
_tz = tzString
|
_tz = tzString
|
||||||
Await UpdateDatabaseAsync()
|
UpdateDatabase()
|
||||||
End Function
|
End SyncLock
|
||||||
|
End Sub
|
||||||
|
|
||||||
Public Async Function UpdateModeratedModeAsync(isModerated As Boolean) As Task
|
Public Sub UpdateModeratedMode(isModerated As Boolean)
|
||||||
|
SyncLock Me
|
||||||
_moderated = isModerated
|
_moderated = isModerated
|
||||||
Await UpdateDatabaseAsync()
|
UpdateDatabase()
|
||||||
End Function
|
End SyncLock
|
||||||
|
End Sub
|
||||||
|
|
||||||
Public Async Function UpdateModeratorRoleAsync(roleId As ULong?) As Task
|
Public Sub UpdateModeratorRole(roleId As ULong?)
|
||||||
|
SyncLock Me
|
||||||
_modRole = roleId
|
_modRole = roleId
|
||||||
Await UpdateDatabaseAsync()
|
UpdateDatabase()
|
||||||
End Function
|
End SyncLock
|
||||||
|
End Sub
|
||||||
|
|
||||||
Public Async Function UpdateAnnounceMessageAsync(message As String, plural As Boolean) As Task
|
Public Sub UpdateAnnounceMessage(message As String, plural As Boolean)
|
||||||
|
SyncLock Me
|
||||||
If plural Then
|
If plural Then
|
||||||
_announceMsgPl = message
|
_announceMsgPl = message
|
||||||
Else
|
Else
|
||||||
_announceMsg = message
|
_announceMsg = message
|
||||||
End If
|
End If
|
||||||
|
|
||||||
Await UpdateDatabaseAsync()
|
UpdateDatabase()
|
||||||
End Function
|
End SyncLock
|
||||||
|
End Sub
|
||||||
|
|
||||||
Public Async Function UpdateAnnouncePingAsync(value As Boolean) As Task
|
Public Sub UpdateAnnouncePing(value As Boolean)
|
||||||
|
SyncLock Me
|
||||||
_announcePing = value
|
_announcePing = value
|
||||||
Await UpdateDatabaseAsync()
|
UpdateDatabase()
|
||||||
End Function
|
End SyncLock
|
||||||
|
End Sub
|
||||||
|
|
||||||
#Region "Database"
|
#Region "Database"
|
||||||
Public Const BackingTable = "settings"
|
Public Const BackingTable = "settings"
|
||||||
|
@ -374,8 +419,8 @@ Friend Class GuildStateInformation
|
||||||
''' Updates the backing database with values from this instance
|
''' Updates the backing database with values from this instance
|
||||||
''' This is a non-asynchronous operation. That may be bad.
|
''' This is a non-asynchronous operation. That may be bad.
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Private Async Function UpdateDatabaseAsync() As Task
|
Private Sub UpdateDatabase()
|
||||||
Using db = Await _db.OpenConnectionAsync()
|
Using db = _db.OpenConnectionAsync().GetAwaiter().GetResult()
|
||||||
Using c = db.CreateCommand()
|
Using c = db.CreateCommand()
|
||||||
c.CommandText = $"update {BackingTable} set " +
|
c.CommandText = $"update {BackingTable} set " +
|
||||||
"role_id = @RoleId, " +
|
"role_id = @RoleId, " +
|
||||||
|
@ -433,9 +478,9 @@ Friend Class GuildStateInformation
|
||||||
End With
|
End With
|
||||||
c.Parameters.Add("@AnnouncePing", NpgsqlDbType.Boolean).Value = _announcePing
|
c.Parameters.Add("@AnnouncePing", NpgsqlDbType.Boolean).Value = _announcePing
|
||||||
c.Prepare()
|
c.Prepare()
|
||||||
Await c.ExecuteNonQueryAsync()
|
c.ExecuteNonQuery()
|
||||||
End Using
|
End Using
|
||||||
End Using
|
End Using
|
||||||
End Function
|
End Sub
|
||||||
#End Region
|
#End Region
|
||||||
End Class
|
End Class
|
||||||
|
|
|
@ -24,11 +24,7 @@ Class ListingCommands
|
||||||
' Creates a file with all birthdays.
|
' Creates a file with all birthdays.
|
||||||
Private Async Function CmdList(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
|
Private Async Function CmdList(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
|
||||||
' For now, we're restricting this command to moderators only. This may turn into an option later.
|
' For now, we're restricting this command to moderators only. This may turn into an option later.
|
||||||
Dim reqMod As Boolean
|
If Not Instance.GuildCache(reqChannel.Guild.Id).IsUserModerator(reqUser) Then
|
||||||
SyncLock Instance.KnownGuilds
|
|
||||||
reqMod = Instance.KnownGuilds(reqChannel.Guild.Id).IsUserModerator(reqUser)
|
|
||||||
End SyncLock
|
|
||||||
If Not reqMod Then
|
|
||||||
Await reqChannel.SendMessageAsync(":x: Only bot moderators may use this command.")
|
Await reqChannel.SendMessageAsync(":x: Only bot moderators may use this command.")
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
@ -126,10 +122,7 @@ Class ListingCommands
|
||||||
''' Users currently not in the guild are not included in the result.
|
''' Users currently not in the guild are not included in the result.
|
||||||
''' </summary>
|
''' </summary>
|
||||||
Private Async Function LoadList(guild As SocketGuild, escapeFormat As Boolean) As Task(Of List(Of ListItem))
|
Private Async Function LoadList(guild As SocketGuild, escapeFormat As Boolean) As Task(Of List(Of ListItem))
|
||||||
Dim ping As Boolean
|
Dim ping = Instance.GuildCache(guild.Id).AnnouncePing
|
||||||
SyncLock Instance.KnownGuilds
|
|
||||||
ping = Instance.KnownGuilds(guild.Id).AnnouncePing
|
|
||||||
End SyncLock
|
|
||||||
|
|
||||||
Using db = Await BotConfig.DatabaseSettings.OpenConnectionAsync()
|
Using db = Await BotConfig.DatabaseSettings.OpenConnectionAsync()
|
||||||
Using c = db.CreateCommand()
|
Using c = db.CreateCommand()
|
||||||
|
|
|
@ -42,11 +42,7 @@ Friend Class ManagerCommands
|
||||||
Private Async Function CmdConfigDispatch(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
|
Private Async Function CmdConfigDispatch(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
|
||||||
' Ignore those without the proper permissions.
|
' Ignore those without the proper permissions.
|
||||||
' Requires either the manage guild permission or to be in the moderators role
|
' Requires either the manage guild permission or to be in the moderators role
|
||||||
Dim allowed As Boolean
|
If Not Instance.GuildCache(reqUser.Guild.Id).IsUserModerator(reqUser) Then
|
||||||
SyncLock Instance.KnownGuilds
|
|
||||||
allowed = Instance.KnownGuilds(reqUser.Guild.Id).IsUserModerator(reqUser)
|
|
||||||
End SyncLock
|
|
||||||
If Not allowed Then
|
|
||||||
Await reqChannel.SendMessageAsync(":x: This command may only be used by bot moderators.")
|
Await reqChannel.SendMessageAsync(":x: This command may only be used by bot moderators.")
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
@ -88,9 +84,7 @@ Friend Class ManagerCommands
|
||||||
ElseIf role.Id = reqChannel.Guild.EveryoneRole.Id Then
|
ElseIf role.Id = reqChannel.Guild.EveryoneRole.Id Then
|
||||||
Await reqChannel.SendMessageAsync(":x: You cannot set that as the birthday role.")
|
Await reqChannel.SendMessageAsync(":x: You cannot set that as the birthday role.")
|
||||||
Else
|
Else
|
||||||
SyncLock Instance.KnownGuilds
|
Instance.GuildCache(guild.Id).UpdateRole(role.Id)
|
||||||
Instance.KnownGuilds(guild.Id).UpdateRoleAsync(role.Id).Wait()
|
|
||||||
End SyncLock
|
|
||||||
Await reqChannel.SendMessageAsync($":white_check_mark: The birthday role has been set as **{role.Name}**.")
|
Await reqChannel.SendMessageAsync($":white_check_mark: The birthday role has been set as **{role.Name}**.")
|
||||||
End If
|
End If
|
||||||
End Function
|
End Function
|
||||||
|
@ -116,9 +110,7 @@ Friend Class ManagerCommands
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
|
||||||
SyncLock Instance.KnownGuilds
|
Instance.GuildCache(reqChannel.Guild.Id).UpdateAnnouncePing(setting)
|
||||||
Instance.KnownGuilds(reqChannel.Guild.Id).UpdateAnnouncePingAsync(setting).Wait()
|
|
||||||
End SyncLock
|
|
||||||
Await reqChannel.SendMessageAsync(result)
|
Await reqChannel.SendMessageAsync(result)
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
|
@ -126,8 +118,7 @@ Friend Class ManagerCommands
|
||||||
Private Async Function ScmdChannel(param As String(), reqChannel As SocketTextChannel) As Task
|
Private Async Function ScmdChannel(param As String(), reqChannel As SocketTextChannel) As Task
|
||||||
If param.Length = 1 Then
|
If param.Length = 1 Then
|
||||||
' No extra parameter. Unset announcement channel.
|
' No extra parameter. Unset announcement channel.
|
||||||
SyncLock Instance.KnownGuilds
|
Dim gi = Instance.GuildCache(reqChannel.Guild.Id)
|
||||||
Dim gi = Instance.KnownGuilds(reqChannel.Guild.Id)
|
|
||||||
|
|
||||||
' Extra detail: Show a unique message if a channel hadn't been set prior.
|
' Extra detail: Show a unique message if a channel hadn't been set prior.
|
||||||
If Not gi.AnnounceChannelId.HasValue Then
|
If Not gi.AnnounceChannelId.HasValue Then
|
||||||
|
@ -135,9 +126,7 @@ Friend Class ManagerCommands
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
|
||||||
gi.UpdateAnnounceChannelAsync(Nothing).Wait()
|
gi.UpdateAnnounceChannel(Nothing)
|
||||||
End SyncLock
|
|
||||||
|
|
||||||
Await reqChannel.SendMessageAsync(":white_check_mark: The announcement channel has been unset.")
|
Await reqChannel.SendMessageAsync(":white_check_mark: The announcement channel has been unset.")
|
||||||
Else
|
Else
|
||||||
' Parameter check: This needs a channel mention to function.
|
' Parameter check: This needs a channel mention to function.
|
||||||
|
@ -156,10 +145,8 @@ Friend Class ManagerCommands
|
||||||
End If
|
End If
|
||||||
|
|
||||||
' Update the value
|
' Update the value
|
||||||
SyncLock Instance.KnownGuilds
|
Dim gi = Instance.GuildCache(reqChannel.Guild.Id)
|
||||||
Dim gi = Instance.KnownGuilds(reqChannel.Guild.Id)
|
gi.UpdateAnnounceChannel(chId)
|
||||||
gi.UpdateAnnounceChannelAsync(chId).Wait()
|
|
||||||
End SyncLock
|
|
||||||
|
|
||||||
' Report the success
|
' Report the success
|
||||||
Await reqChannel.SendMessageAsync($":white_check_mark: The announcement channel is now set to <#{chId}>.")
|
Await reqChannel.SendMessageAsync($":white_check_mark: The announcement channel is now set to <#{chId}>.")
|
||||||
|
@ -178,9 +165,7 @@ Friend Class ManagerCommands
|
||||||
If role Is Nothing Then
|
If role Is Nothing Then
|
||||||
Await reqChannel.SendMessageAsync(RoleInputError)
|
Await reqChannel.SendMessageAsync(RoleInputError)
|
||||||
Else
|
Else
|
||||||
SyncLock Instance.KnownGuilds
|
Instance.GuildCache(guild.Id).UpdateModeratorRole(role.Id)
|
||||||
Instance.KnownGuilds(guild.Id).UpdateModeratorRoleAsync(role.Id).Wait()
|
|
||||||
End SyncLock
|
|
||||||
Await reqChannel.SendMessageAsync($":white_check_mark: The moderator role is now **{role.Name}**.")
|
Await reqChannel.SendMessageAsync($":white_check_mark: The moderator role is now **{role.Name}**.")
|
||||||
End If
|
End If
|
||||||
End Function
|
End Function
|
||||||
|
@ -189,8 +174,7 @@ Friend Class ManagerCommands
|
||||||
Private Async Function ScmdZone(param As String(), reqChannel As SocketTextChannel) As Task
|
Private Async Function ScmdZone(param As String(), reqChannel As SocketTextChannel) As Task
|
||||||
If param.Length = 1 Then
|
If param.Length = 1 Then
|
||||||
' No extra parameter. Unset guild default time zone.
|
' No extra parameter. Unset guild default time zone.
|
||||||
SyncLock Instance.KnownGuilds
|
Dim gi = Instance.GuildCache(reqChannel.Guild.Id)
|
||||||
Dim gi = Instance.KnownGuilds(reqChannel.Guild.Id)
|
|
||||||
|
|
||||||
' Extra detail: Show a unique message if there is no set zone.
|
' Extra detail: Show a unique message if there is no set zone.
|
||||||
If Not gi.AnnounceChannelId.HasValue Then
|
If Not gi.AnnounceChannelId.HasValue Then
|
||||||
|
@ -198,8 +182,7 @@ Friend Class ManagerCommands
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
|
||||||
gi.UpdateTimeZoneAsync(Nothing).Wait()
|
gi.UpdateTimeZone(Nothing)
|
||||||
End SyncLock
|
|
||||||
|
|
||||||
Await reqChannel.SendMessageAsync(":white_check_mark: The default time zone preference has been removed.")
|
Await reqChannel.SendMessageAsync(":white_check_mark: The default time zone preference has been removed.")
|
||||||
Else
|
Else
|
||||||
|
@ -213,10 +196,7 @@ Friend Class ManagerCommands
|
||||||
End Try
|
End Try
|
||||||
|
|
||||||
' Update value
|
' Update value
|
||||||
SyncLock Instance.KnownGuilds
|
Instance.GuildCache(reqChannel.Guild.Id).UpdateTimeZone(zone)
|
||||||
Dim gi = Instance.KnownGuilds(reqChannel.Guild.Id)
|
|
||||||
gi.UpdateTimeZoneAsync(zone).Wait()
|
|
||||||
End SyncLock
|
|
||||||
|
|
||||||
' Report the success
|
' Report the success
|
||||||
Await reqChannel.SendMessageAsync($":white_check_mark: The server's time zone has been set to **{zone}**.")
|
Await reqChannel.SendMessageAsync($":white_check_mark: The server's time zone has been set to **{zone}**.")
|
||||||
|
@ -238,26 +218,23 @@ Friend Class ManagerCommands
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
|
||||||
SyncLock Instance.KnownGuilds
|
Dim gi = Instance.GuildCache(reqChannel.Guild.Id)
|
||||||
Dim gi = Instance.KnownGuilds(reqChannel.Guild.Id)
|
Dim isBanned = Await gi.IsUserBlockedAsync(inputId)
|
||||||
Dim isBanned = gi.IsUserBlockedAsync(inputId).GetAwaiter().GetResult()
|
|
||||||
|
|
||||||
If doBan Then
|
If doBan Then
|
||||||
If Not isBanned Then
|
If Not isBanned Then
|
||||||
gi.BlockUserAsync(inputId).Wait()
|
Await gi.BlockUserAsync(inputId)
|
||||||
reqChannel.SendMessageAsync(":white_check_mark: User has been blocked.").Wait()
|
reqChannel.SendMessageAsync(":white_check_mark: User has been blocked.").Wait()
|
||||||
Else
|
Else
|
||||||
reqChannel.SendMessageAsync(":white_check_mark: User is already blocked.").Wait()
|
reqChannel.SendMessageAsync(":white_check_mark: User is already blocked.").Wait()
|
||||||
End If
|
End If
|
||||||
Else
|
Else
|
||||||
If isBanned Then
|
If isBanned Then
|
||||||
gi.UnbanUserAsync(inputId).Wait()
|
Await gi.UnbanUserAsync(inputId)
|
||||||
reqChannel.SendMessageAsync(":white_check_mark: User is now unblocked.").Wait()
|
reqChannel.SendMessageAsync(":white_check_mark: User is now unblocked.").Wait()
|
||||||
Else
|
Else
|
||||||
reqChannel.SendMessageAsync(":white_check_mark: The specified user has not been blocked.").Wait()
|
reqChannel.SendMessageAsync(":white_check_mark: The specified user has not been blocked.").Wait()
|
||||||
End If
|
End If
|
||||||
End If
|
End If
|
||||||
End SyncLock
|
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
' "moderated on/off" - Sets/unsets moderated mode.
|
' "moderated on/off" - Sets/unsets moderated mode.
|
||||||
|
@ -278,13 +255,9 @@ Friend Class ManagerCommands
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
|
||||||
Dim currentSet As Boolean
|
Dim gi = Instance.GuildCache(reqChannel.Guild.Id)
|
||||||
|
Dim currentSet = gi.IsModerated
|
||||||
SyncLock Instance.KnownGuilds
|
gi.UpdateModeratedMode(modSet)
|
||||||
Dim gi = Instance.KnownGuilds(reqChannel.Guild.Id)
|
|
||||||
currentSet = gi.IsModerated
|
|
||||||
gi.UpdateModeratedModeAsync(modSet).Wait()
|
|
||||||
End SyncLock
|
|
||||||
|
|
||||||
If currentSet = modSet Then
|
If currentSet = modSet Then
|
||||||
Await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode is already {parameter}.")
|
Await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode is already {parameter}.")
|
||||||
|
@ -307,9 +280,7 @@ Friend Class ManagerCommands
|
||||||
clear = True
|
clear = True
|
||||||
End If
|
End If
|
||||||
|
|
||||||
SyncLock Instance.KnownGuilds
|
Instance.GuildCache(reqChannel.Guild.Id).UpdateAnnounceMessage(newmsg, plural)
|
||||||
Instance.KnownGuilds(reqChannel.Guild.Id).UpdateAnnounceMessageAsync(newmsg, plural).Wait()
|
|
||||||
End SyncLock
|
|
||||||
Const report = ":white_check_mark: The {0} birthday announcement message has been {1}."
|
Const report = ":white_check_mark: The {0} birthday announcement message has been {1}."
|
||||||
Await reqChannel.SendMessageAsync(String.Format(report, If(plural, "plural", "singular"), If(clear, "reset", "updated")))
|
Await reqChannel.SendMessageAsync(String.Format(report, If(plural, "plural", "singular"), If(clear, "reset", "updated")))
|
||||||
End Function
|
End Function
|
||||||
|
@ -318,9 +289,7 @@ Friend Class ManagerCommands
|
||||||
' Execute command as another user
|
' Execute command as another user
|
||||||
Private Async Function CmdOverride(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
|
Private Async Function CmdOverride(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
|
||||||
' Moderators only. As with config, silently drop if this check fails.
|
' Moderators only. As with config, silently drop if this check fails.
|
||||||
SyncLock Instance.KnownGuilds
|
If Not Instance.GuildCache(reqUser.Guild.Id).IsUserModerator(reqUser) Then Return
|
||||||
If Not Instance.KnownGuilds(reqUser.Guild.Id).IsUserModerator(reqUser) Then Return
|
|
||||||
End SyncLock
|
|
||||||
|
|
||||||
If param.Length <> 3 Then
|
If param.Length <> 3 Then
|
||||||
Await reqChannel.SendMessageAsync(GenericError)
|
Await reqChannel.SendMessageAsync(GenericError)
|
||||||
|
|
|
@ -104,11 +104,9 @@ Class UserCommands
|
||||||
' Parsing successful. Update user information.
|
' Parsing successful. Update user information.
|
||||||
Dim known As Boolean ' Extra detail: Bot's response changes if the user was previously unknown.
|
Dim known As Boolean ' Extra detail: Bot's response changes if the user was previously unknown.
|
||||||
Try
|
Try
|
||||||
SyncLock Instance.KnownGuilds
|
Dim user = Instance.GuildCache(reqChannel.Guild.Id).GetUser(reqUser.Id)
|
||||||
Dim user = Instance.KnownGuilds(reqChannel.Guild.Id).GetUser(reqUser.Id)
|
|
||||||
known = user.IsKnown
|
known = user.IsKnown
|
||||||
user.UpdateAsync(bmonth, bday, btz, BotConfig.DatabaseSettings).Wait()
|
Await user.UpdateAsync(bmonth, bday, btz, BotConfig.DatabaseSettings)
|
||||||
End SyncLock
|
|
||||||
Catch ex As Exception
|
Catch ex As Exception
|
||||||
Log("Error", ex.ToString())
|
Log("Error", ex.ToString())
|
||||||
reqChannel.SendMessageAsync(":x: An unknown error occurred. The bot owner has been notified.").Wait()
|
reqChannel.SendMessageAsync(":x: An unknown error occurred. The bot owner has been notified.").Wait()
|
||||||
|
@ -128,10 +126,9 @@ Class UserCommands
|
||||||
End If
|
End If
|
||||||
|
|
||||||
Dim btz As String = Nothing
|
Dim btz As String = Nothing
|
||||||
SyncLock Instance.KnownGuilds
|
Dim user = Instance.GuildCache(reqChannel.Guild.Id).GetUser(reqUser.Id)
|
||||||
Dim user = Instance.KnownGuilds(reqChannel.Guild.Id).GetUser(reqUser.Id)
|
|
||||||
If Not user.IsKnown Then
|
If Not user.IsKnown Then
|
||||||
reqChannel.SendMessageAsync(":x: Can't set your time zone if your birth date isn't registered.").Wait()
|
Await reqChannel.SendMessageAsync(":x: Can't set your time zone if your birth date isn't registered.")
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
|
||||||
|
@ -141,8 +138,8 @@ Class UserCommands
|
||||||
reqChannel.SendMessageAsync(ex.Message).Wait()
|
reqChannel.SendMessageAsync(ex.Message).Wait()
|
||||||
Return
|
Return
|
||||||
End Try
|
End Try
|
||||||
user.UpdateAsync(user.BirthMonth, user.BirthDay, btz, BotConfig.DatabaseSettings).Wait()
|
Await user.UpdateAsync(user.BirthMonth, user.BirthDay, btz, BotConfig.DatabaseSettings)
|
||||||
End SyncLock
|
|
||||||
Await reqChannel.SendMessageAsync($":white_check_mark: Your time zone has been updated to **{btz}**.")
|
Await reqChannel.SendMessageAsync($":white_check_mark: Your time zone has been updated to **{btz}**.")
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
|
@ -155,13 +152,10 @@ Class UserCommands
|
||||||
|
|
||||||
' Extra detail: Send a notification if the user isn't actually known by the bot.
|
' Extra detail: Send a notification if the user isn't actually known by the bot.
|
||||||
Dim known As Boolean
|
Dim known As Boolean
|
||||||
SyncLock Instance.KnownGuilds
|
Dim g = Instance.GuildCache(reqChannel.Guild.Id)
|
||||||
Dim g = Instance.KnownGuilds(reqChannel.Guild.Id)
|
|
||||||
known = g.GetUser(reqUser.Id).IsKnown
|
known = g.GetUser(reqUser.Id).IsKnown
|
||||||
If known Then
|
' Delete database and cache entry
|
||||||
g.DeleteUserAsync(reqUser.Id).Wait()
|
Await g.DeleteUserAsync(reqUser.Id)
|
||||||
End If
|
|
||||||
End SyncLock
|
|
||||||
If Not known Then
|
If Not known Then
|
||||||
Await reqChannel.SendMessageAsync(":white_check_mark: I don't have your information. Nothing to remove.")
|
Await reqChannel.SendMessageAsync(":white_check_mark: I don't have your information. Nothing to remove.")
|
||||||
Else
|
Else
|
||||||
|
|
Loading…
Reference in a new issue