Implemented a SocialManager with support for posting to both Twitter and Mastodon

This commit is contained in:
Erik C. Thauvin 2022-12-05 23:50:16 -08:00
parent 6a3caa84ce
commit 6fd59e9487
42 changed files with 642 additions and 269 deletions

View file

@ -57,6 +57,10 @@ jobs:
TWITTER_HANDLE: ${{ secrets.TWITTER_HANDLE }}
TWITTER_TOKEN: ${{ secrets.TWITTER_TOKEN }}
TWITTER_TOKENSECRET: ${{ secrets.TWITTER_TOKENSECRET }}
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
MASTODON_HANDLE: ${{ secrets.MASTODON_HANDLE }}
MASTODON_INSTANCE: ${{ secrets.MASTODON_INSTANCE }}
run: ./gradlew build check --stacktrace
- name: SonarCloud

View file

@ -2,9 +2,9 @@
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>CyclomaticComplexMethod:FeedsMgr.kt$FeedsMgr.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>CyclomaticComplexMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>CyclomaticComplexMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>LongMethod:FeedsMgr.kt$FeedsMgr.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>LongMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>LongMethod:Mobibot.kt$Mobibot.Companion$@JvmStatic @Throws(Exception::class) fun main(args: Array&lt;String&gt;)</ID>
<ID>LongMethod:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>LongMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
@ -23,13 +23,14 @@
<ID>MagicNumber:Info.kt$Info.Companion$30</ID>
<ID>MagicNumber:Info.kt$Info.Companion$365</ID>
<ID>MagicNumber:Info.kt$Info.Companion$7</ID>
<ID>MagicNumber:Mastodon.kt$Mastodon.Companion$200</ID>
<ID>MagicNumber:Mobibot.kt$Mobibot$8</ID>
<ID>MagicNumber:Modules.kt$Modules$7</ID>
<ID>MagicNumber:SocialManager.kt$SocialManager$1000L</ID>
<ID>MagicNumber:SocialManager.kt$SocialManager$60L</ID>
<ID>MagicNumber:StockQuote.kt$StockQuote.Companion$10</ID>
<ID>MagicNumber:Tell.kt$Tell$50</ID>
<ID>MagicNumber:Tell.kt$Tell$7</ID>
<ID>MagicNumber:Twitter.kt$Twitter$1000L</ID>
<ID>MagicNumber:Twitter.kt$Twitter$60L</ID>
<ID>MagicNumber:TwitterOAuth.kt$TwitterOAuth$401</ID>
<ID>MagicNumber:Users.kt$Users$8</ID>
<ID>MagicNumber:Utils.kt$Utils$200</ID>
@ -45,18 +46,19 @@
<ID>MagicNumber:WorldTime.kt$WorldTime.Companion$60</ID>
<ID>MagicNumber:WorldTime.kt$WorldTime.Companion$86.4</ID>
<ID>MaxLineLength:TwitterOAuth.kt$TwitterOAuth$*</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(command: AbstractCommand)</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule)</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(command: AbstractCommand): Boolean</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule): Boolean</ID>
<ID>NestedBlockDepth:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?): String</ID>
<ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(query: String): Message</ID>
<ID>NestedBlockDepth:EntryLink.kt$EntryLink$private fun setTags(tags: List&lt;String?&gt;)</ID>
<ID>NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$@JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = currentXml): String</ID>
<ID>NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = currentXml): String</ID>
<ID>NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>NestedBlockDepth:GoogleSearch.kt$GoogleSearch$override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List&lt;Message&gt;</ID>
<ID>NestedBlockDepth:LinksMgr.kt$LinksMgr$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:LinksManager.kt$LinksManager$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Lookup.kt$Lookup$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Mastodon.kt$Mastodon.Companion$@JvmStatic @Throws(ModuleException::class) fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String</ID>
<ID>NestedBlockDepth:Posting.kt$Posting$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Seen.kt$Seen$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
@ -77,6 +79,7 @@
<ID>ThrowsCount:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?): String</ID>
<ID>ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List&lt;Message&gt;</ID>
<ID>ThrowsCount:Joke.kt$Joke.Companion$@JvmStatic @Throws(ModuleException::class) fun randomJoke(): List&lt;Message&gt;</ID>
<ID>ThrowsCount:Mastodon.kt$Mastodon.Companion$@JvmStatic @Throws(ModuleException::class) fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject</ID>
<ID>ThrowsCount:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>

View file

@ -41,12 +41,25 @@ tell-max-size=50
#twitter-token=
#twitter-tokenSecret=
# Twitter handle to receive channel join notifications
# Twitter handle to receive channel join/leave notifications
#twitter-handle=
# Automatically post links to twitter
# Automatically post links to Mastodon
#twitter-auto-post=true
#
# Create a Mastodon application access token at: https//SERVER_INSTANCE/settings/applications
# Make sure the 'write:statuses' scope is enabled.
#
#mastodon-access-token=
#mastodon-instance=mastodon.social
# Mastodon handle to receive channel join/leave notifications
#mastodon-handle=@mobitopia
# Automatically post links to Mastodon
#mastodon-auto-post=true
#
# Create custom search engine at: https://cse.google.com/
# and get API key from: https://console.developers.google.com/
@ -65,7 +78,7 @@ tell-max-size=50
#alphavantage-api-key=
#
# Get Wolfram Alpa AppID from: https://developer.wolframalpha.com/portal/
# Get Wolfram Alpha AppID from: https://developer.wolframalpha.com/portal/
#
#wolfram-appid=
#wolfram-units=imperial

View file

@ -33,7 +33,7 @@ package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.Utils.notContains
import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.commands.links.LinksMgr
import net.thauvin.erik.mobibot.commands.links.LinksManager
import net.thauvin.erik.mobibot.modules.AbstractModule
import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent
@ -46,8 +46,8 @@ import java.util.Properties
*/
class Addons(private val props: Properties) {
private val logger: Logger = LoggerFactory.getLogger(Addons::class.java)
private val disabledModules = props.getProperty("disabled-modules", "").split(LinksMgr.TAG_MATCH.toRegex())
private val disableCommands = props.getProperty("disabled-commands", "").split(LinksMgr.TAG_MATCH.toRegex())
private val disabledModules = props.getProperty("disabled-modules", "").split(LinksManager.TAG_MATCH.toRegex())
private val disableCommands = props.getProperty("disabled-commands", "").split(LinksManager.TAG_MATCH.toRegex())
val commands: MutableList<AbstractCommand> = mutableListOf()
val modules: MutableList<AbstractModule> = mutableListOf()
@ -56,7 +56,8 @@ class Addons(private val props: Properties) {
/**
* Add a module with properties.
*/
fun add(module: AbstractModule) {
fun add(module: AbstractModule): Boolean {
var enabled = false
with(module) {
if (disabledModules.notContains(name, true)) {
if (hasProperties()) {
@ -69,6 +70,7 @@ class Addons(private val props: Properties) {
modules.add(this)
names.modules.add(name)
names.commands.addAll(commands)
enabled = true
} else {
if (logger.isDebugEnabled) {
logger.debug("Module $name is disabled.")
@ -76,12 +78,14 @@ class Addons(private val props: Properties) {
}
}
}
return enabled
}
/**
* Add a command with properties.
*/
fun add(command: AbstractCommand) {
fun add(command: AbstractCommand): Boolean {
var enabled = false
with(command) {
if (disableCommands.notContains(name, true)) {
if (properties.isNotEmpty()) {
@ -98,6 +102,7 @@ class Addons(private val props: Properties) {
names.commands.add(name)
}
}
enabled = true
} else {
if (logger.isDebugEnabled) {
logger.debug("Command $name is disabled.")
@ -105,6 +110,7 @@ class Addons(private val props: Properties) {
}
}
}
return enabled
}
/**

View file

@ -37,7 +37,7 @@ import com.rometools.rome.io.XmlReader
import net.thauvin.erik.mobibot.Utils.green
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.entries.FeedsMgr
import net.thauvin.erik.mobibot.entries.FeedsManager
import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.NoticeMessage
import org.pircbotx.hooks.types.GenericMessageEvent
@ -50,7 +50,7 @@ import java.net.URL
* Reads an RSS feed.
*/
class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable {
private val logger: Logger = LoggerFactory.getLogger(FeedsMgr::class.java)
private val logger: Logger = LoggerFactory.getLogger(FeedsManager::class.java)
/**
* Fetches the Feed's items.

View file

@ -61,7 +61,7 @@ import net.thauvin.erik.mobibot.commands.Say
import net.thauvin.erik.mobibot.commands.Users
import net.thauvin.erik.mobibot.commands.Versions
import net.thauvin.erik.mobibot.commands.links.Comment
import net.thauvin.erik.mobibot.commands.links.LinksMgr
import net.thauvin.erik.mobibot.commands.links.LinksManager
import net.thauvin.erik.mobibot.commands.links.Posting
import net.thauvin.erik.mobibot.commands.links.Tags
import net.thauvin.erik.mobibot.commands.links.View
@ -75,9 +75,11 @@ import net.thauvin.erik.mobibot.modules.Dice
import net.thauvin.erik.mobibot.modules.GoogleSearch
import net.thauvin.erik.mobibot.modules.Joke
import net.thauvin.erik.mobibot.modules.Lookup
import net.thauvin.erik.mobibot.modules.Mastodon
import net.thauvin.erik.mobibot.modules.Ping
import net.thauvin.erik.mobibot.modules.RockPaperScissors
import net.thauvin.erik.mobibot.modules.StockQuote
import net.thauvin.erik.mobibot.modules.Twitter
import net.thauvin.erik.mobibot.modules.War
import net.thauvin.erik.mobibot.modules.Weather2
import net.thauvin.erik.mobibot.modules.WolframAlpha
@ -146,7 +148,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
)
event.sendMessage("The commands are:")
event.sendList(addons.names.commands, 8, isBold = true, isIndent = true)
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
event.sendMessage("The op commands are:")
event.sendList(addons.names.ops, 8, isBold = true, isIndent = true)
}
@ -174,11 +176,11 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
override fun onDisconnect(event: DisconnectEvent?) {
event?.let {
with(event.getBot<PircBotX>()) {
LinksMgr.twitter.notification("$nick disconnected from irc://$serverHostname")
LinksManager.socialManager.notification("$nick disconnected from irc://$serverHostname")
seen.add(userChannelDao.getChannel(channel).users)
}
}
LinksMgr.twitter.shutdown()
LinksManager.socialManager.shutdown()
}
override fun onPrivateMessage(event: PrivateMessageEvent?) {
@ -199,7 +201,9 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
event?.user?.let { user ->
with(event.getBot<PircBotX>()) {
if (user.nick == nick) {
LinksMgr.twitter.notification("$nick has joined ${event.channel.name} on irc://$serverHostname")
LinksManager.socialManager.notification(
"$nick has joined ${event.channel.name} on irc://$serverHostname"
)
seen.add(userChannelDao.getChannel(channel).users)
} else {
tell.send(event)
@ -247,7 +251,9 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
event?.user?.let { user ->
with(event.getBot<PircBotX>()) {
if (user.nick == nick) {
LinksMgr.twitter.notification("$nick has left ${event.channel.name} on irc://$serverHostname")
LinksManager.socialManager.notification(
"$nick has left ${event.channel.name} on irc://$serverHostname"
)
seen.add(userChannelDao.getChannel(channel).users)
} else {
seen.add(user.nick)
@ -388,7 +394,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
}.buildConfiguration()
// Load the current entries
with(LinksMgr) {
with(LinksManager) {
entries.channel = channel
entries.ircServer = ircServer
entries.logsDir = logsDirPath
@ -407,7 +413,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
addons.add(Cycle())
addons.add(Die())
addons.add(Ignore())
addons.add(LinksMgr())
addons.add(LinksManager())
addons.add(Me())
addons.add(Modules(addons.names.modules))
addons.add(Msg())
@ -426,11 +432,13 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
tell = Tell("${logsDirPath}${nickname}.ser")
addons.add(tell)
addons.add(LinksMgr.twitter)
addons.add(Users())
addons.add(Versions())
addons.add(View())
// Load social modules
LinksManager.socialManager.add(addons, Twitter(), Mastodon())
// Load the modules
addons.add(Calc())
addons.add(ChatGpt())

View file

@ -65,6 +65,18 @@ import kotlin.io.path.fileSize
object Utils {
private val searchFlags = arrayOf("%c", "%n")
/**
* Prepends a prefix if not present.
*/
@JvmStatic
fun String.prefixIfMissing(prefix: Char): String {
return if (first() != prefix) {
"$prefix${this}"
} else {
this
}
}
/**
* Appends a suffix to the end of the String if not present.
*/
@ -183,8 +195,8 @@ object Utils {
* Returns {@code true} if the specified user is an operator on the [channel].
*/
@JvmStatic
fun isChannelOp(channel: String, event: GenericMessageEvent): Boolean {
return event.bot().userChannelDao.getChannel(channel).isOp(event.user)
fun GenericMessageEvent.isChannelOp(channel: String): Boolean {
return this.bot().userChannelDao.getChannel(channel).isOp(this.user)
}
/**

View file

@ -51,7 +51,7 @@ abstract class AbstractCommand {
abstract fun commandResponse(channel: String, args: String, event: GenericMessageEvent)
open fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
if (!isOpOnly || isOpOnly == isChannelOp(channel, event)) {
if (!isOpOnly || isOpOnly == event.isChannelOp(channel)) {
for (h in help) {
event.sendMessage(helpCmdSyntax(h, event.bot().nick, event is PrivateMessageEvent || !isPublic))
}

View file

@ -49,7 +49,7 @@ class Cycle : AbstractCommand() {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
with(event.bot()) {
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
runBlocking {
sendIRC().message(channel, "${event.user.nick} asked me to leave. I'll be back!")
userChannelDao.getChannel(channel).send().part()

View file

@ -45,7 +45,7 @@ class Die : AbstractCommand() {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
with(event.bot()) {
if (isChannelOp(channel, event) && (properties[DIE_PROP].isNullOrBlank() || args == properties[DIE_PROP])) {
if (event.isChannelOp(channel) && (properties[DIE_PROP].isNullOrBlank() || args == properties[DIE_PROP])) {
sendIRC().message(channel, "${event.user?.nick} has just signed my death sentence.")
stopBotReconnect()
sendIRC().quitServer("The Bot is Out There!")

View file

@ -39,7 +39,7 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import net.thauvin.erik.mobibot.Utils.sendList
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.links.LinksMgr
import net.thauvin.erik.mobibot.commands.links.LinksManager
import org.pircbotx.hooks.types.GenericMessageEvent
class Ignore : AbstractCommand() {
@ -79,7 +79,7 @@ class Ignore : AbstractCommand() {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val isMe = args.trim().equals(me, true)
if (isMe || !isChannelOp(channel, event)) {
if (isMe || !event.isChannelOp(channel)) {
val nick = event.user.nick.lowercase()
ignoreNick(nick, isMe, event)
} else {
@ -88,7 +88,7 @@ class Ignore : AbstractCommand() {
}
override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
return if (isChannelOp(channel, event)) {
return if (event.isChannelOp(channel)) {
for (h in helpOp) {
event.sendMessage(helpCmdSyntax(h, event.bot().nick, true))
}
@ -141,7 +141,7 @@ class Ignore : AbstractCommand() {
override fun setProperty(key: String, value: String) {
super.setProperty(key, value)
if (IGNORE_PROP == key) {
ignored.addAll(value.split(LinksMgr.TAG_MATCH.toRegex()))
ignored.addAll(value.split(LinksManager.TAG_MATCH.toRegex()))
}
}

View file

@ -39,7 +39,7 @@ import net.thauvin.erik.mobibot.Utils.isChannelOp
import net.thauvin.erik.mobibot.Utils.plural
import net.thauvin.erik.mobibot.Utils.sendList
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.links.LinksMgr
import net.thauvin.erik.mobibot.commands.links.LinksManager
import net.thauvin.erik.mobibot.commands.seen.Seen
import net.thauvin.erik.mobibot.commands.tell.Tell
import org.pircbotx.hooks.types.GenericMessageEvent
@ -107,16 +107,16 @@ class Info(private val tell: Tell, private val seen: Seen) : AbstractCommand() {
info.append("Uptime: ")
.append(ManagementFactory.getRuntimeMXBean().uptime.toUptime())
.append(" [Entries: ")
.append(LinksMgr.entries.links.size)
.append(LinksManager.entries.links.size)
if (seen.isEnabled()) {
info.append(", Seen: ").append(seen.count())
}
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
if (tell.isEnabled()) {
info.append(", Messages: ").append(tell.size())
}
if (LinksMgr.twitter.isAutoPost) {
info.append(", Twitter: ").append(LinksMgr.twitter.entriesCount())
if (LinksManager.socialManager.entriesCount() > 0) {
info.append(", Social: ").append(LinksManager.socialManager.entriesCount())
}
}
info.append(", Recap: ").append(Recap.recaps.size).append(']')

View file

@ -45,7 +45,7 @@ class Me : AbstractCommand() {
override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
event.bot().sendIRC().action(channel, args)
}
}

View file

@ -45,7 +45,7 @@ class Modules(private val modules: List<String>) : AbstractCommand() {
override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
if (modules.isEmpty()) {
event.respondPrivateMessage("There are no enabled modules.")
} else {

View file

@ -48,7 +48,7 @@ class Msg : AbstractCommand() {
override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
val msg = args.split(" ", limit = 2)
if (args.length > 2) {
event.bot().sendIRC().message(msg[0], msg[1])

View file

@ -45,7 +45,7 @@ class Nick : AbstractCommand() {
override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
event.bot().sendIRC().changeNick(args)
}
}

View file

@ -45,7 +45,7 @@ class Say : AbstractCommand() {
override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
event.bot().sendIRC().message(channel, args)
}
}

View file

@ -53,7 +53,7 @@ class Versions : AbstractCommand() {
override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
event.sendList(allVersions, 1)
}
}

View file

@ -66,8 +66,8 @@ class Comment : AbstractCommand() {
val cmds = args.substring(1).split("[.:]".toRegex(), 3)
val entryIndex = cmds[0].toInt() - 1
if (entryIndex < LinksMgr.entries.links.size && LinksMgr.isUpToDate(event)) {
val entry: EntryLink = LinksMgr.entries.links[entryIndex]
if (entryIndex < LinksManager.entries.links.size && LinksManager.isUpToDate(event)) {
val entry: EntryLink = LinksManager.entries.links[entryIndex]
val commentIndex = cmds[1].toInt() - 1
if (commentIndex < entry.comments.size) {
when (val cmd = cmds[2].trim()) {
@ -87,7 +87,7 @@ class Comment : AbstractCommand() {
override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
if (super.helpResponse(channel, topic, event)) {
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
event.sendMessage("To change a comment's author:")
event.sendMessage(helpFormat("${Constants.LINK_CMD}1.1:?<nick>"))
}
@ -108,11 +108,11 @@ class Comment : AbstractCommand() {
commentIndex: Int,
event: GenericMessageEvent
) {
if (isChannelOp(channel, event) && cmd.length > 1) {
if (event.isChannelOp(channel) && cmd.length > 1) {
val comment = entry.getComment(commentIndex)
comment.nick = cmd.substring(1)
event.sendMessage(buildComment(entryIndex, commentIndex, comment))
LinksMgr.entries.save()
LinksManager.entries.save()
} else {
event.sendMessage("Please ask a channel op to change the author of this comment for you.")
}
@ -125,10 +125,10 @@ class Comment : AbstractCommand() {
commentIndex: Int,
event: GenericMessageEvent
) {
if (isChannelOp(channel, event) || event.user.nick == entry.getComment(commentIndex).nick) {
if (event.isChannelOp(channel) || event.user.nick == entry.getComment(commentIndex).nick) {
entry.deleteComment(commentIndex)
event.sendMessage("Comment ${entryIndex.toLinkLabel()}.${commentIndex + 1} removed.")
LinksMgr.entries.save()
LinksManager.entries.save()
} else {
event.sendMessage("Please ask a channel op to delete this comment for you.")
}
@ -143,7 +143,7 @@ class Comment : AbstractCommand() {
) {
entry.setComment(commentIndex, cmd, event.user.nick)
event.sendMessage(buildComment(entryIndex, commentIndex, entry.getComment(commentIndex)))
LinksMgr.entries.save()
LinksManager.entries.save()
}
private fun showComment(entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent) {

View file

@ -1,5 +1,5 @@
/*
* LinksMgr.kt
* LinksManager.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@ -45,12 +45,12 @@ import net.thauvin.erik.mobibot.entries.Entries
import net.thauvin.erik.mobibot.entries.EntriesUtils.buildLink
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.mobibot.modules.Twitter
import net.thauvin.erik.mobibot.social.SocialManager
import org.jsoup.Jsoup
import org.pircbotx.hooks.types.GenericMessageEvent
import java.io.IOException
class LinksMgr : AbstractCommand() {
class LinksManager : AbstractCommand() {
private val defaultTags: MutableList<String> = mutableListOf()
private val keywords: MutableList<String> = mutableListOf()
@ -83,10 +83,10 @@ class LinksMgr : AbstractCommand() {
val pinboard = Pinboard()
/**
* Twitter handler.
* Social Manager handler.
*/
@JvmField
val twitter = Twitter()
val socialManager = SocialManager()
/**
* Let the user know if the entries are too old to be modified.
@ -140,8 +140,8 @@ class LinksMgr : AbstractCommand() {
pinboard.addPin(event.bot().serverHostname, entry)
// Queue link for posting to Twitter.
twitter.queueEntry(index)
// Queue link for posting to social media.
socialManager.queueEntry(index)
entries.save()

View file

@ -39,7 +39,7 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.commands.links.LinksMgr.Companion.entries
import net.thauvin.erik.mobibot.commands.links.LinksManager.Companion.entries
import net.thauvin.erik.mobibot.entries.EntriesUtils
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import net.thauvin.erik.mobibot.entries.EntryLink
@ -71,7 +71,7 @@ class Posting : AbstractCommand() {
val cmd = cmds[1].trim()
if (cmd.isBlank()) {
showEntry(entryIndex, event) // L1:
} else if (LinksMgr.isUpToDate(event)) {
} else if (LinksManager.isUpToDate(event)) {
if (cmd == "-") {
removeEntry(channel, entryIndex, event) // L1:-
} else {
@ -102,7 +102,7 @@ class Posting : AbstractCommand() {
if (cmd.length > 1) {
val entry: EntryLink = entries.links[entryIndex]
entry.title = cmd.substring(1).trim()
LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
event.sendMessage(EntriesUtils.buildLink(entryIndex, entry))
entries.save()
}
@ -110,12 +110,12 @@ class Posting : AbstractCommand() {
private fun changeUrl(channel: String, cmd: String, entryIndex: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries.links[entryIndex]
if (entry.login == event.user.login || isChannelOp(channel, event)) {
if (entry.login == event.user.login || event.isChannelOp(channel)) {
val link = cmd.substring(1)
if (link.matches(LinksMgr.LINK_MATCH.toRegex())) {
if (link.matches(LinksManager.LINK_MATCH.toRegex())) {
val oldLink = entry.link
entry.link = link
LinksMgr.pinboard.updatePin(event.bot().serverHostname, oldLink, entry)
LinksManager.pinboard.updatePin(event.bot().serverHostname, oldLink, entry)
event.sendMessage(EntriesUtils.buildLink(entryIndex, entry))
entries.save()
}
@ -123,11 +123,11 @@ class Posting : AbstractCommand() {
}
private fun changeAuthor(channel: String, cmd: String, index: Int, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) {
if (event.isChannelOp(channel)) {
if (cmd.length > 1) {
val entry: EntryLink = entries.links[index]
entry.nick = cmd.substring(1)
LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
event.sendMessage(EntriesUtils.buildLink(index, entry))
entries.save()
}
@ -138,9 +138,9 @@ class Posting : AbstractCommand() {
private fun removeEntry(channel: String, index: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries.links[index]
if (entry.login == event.user.login || isChannelOp(channel, event)) {
LinksMgr.pinboard.deletePin(entry)
LinksMgr.twitter.removeEntry(index)
if (entry.login == event.user.login || event.isChannelOp(channel)) {
LinksManager.pinboard.deletePin(entry)
LinksManager.socialManager.removeEntry(index)
entries.links.removeAt(index)
event.sendMessage("Entry ${index.toLinkLabel()} removed.")
entries.save()

View file

@ -60,15 +60,15 @@ class Tags : AbstractCommand() {
val cmds = args.substring(1).split("${Constants.TAG_CMD}:", limit = 2)
val index = cmds[0].toInt() - 1
if (index < LinksMgr.entries.links.size && LinksMgr.isUpToDate(event)) {
if (index < LinksManager.entries.links.size && LinksManager.isUpToDate(event)) {
val cmd = cmds[1].trim()
val entry: EntryLink = LinksMgr.entries.links[index]
val entry: EntryLink = LinksManager.entries.links[index]
if (cmd.isNotEmpty()) {
if (entry.login == event.user.login || isChannelOp(channel, event)) {
if (entry.login == event.user.login || event.isChannelOp(channel)) {
entry.setTags(cmd)
LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
event.sendMessage(EntriesUtils.buildTags(index, entry))
LinksMgr.entries.save()
LinksManager.entries.save()
} else {
event.sendMessage("Please ask a channel op to change the tags for you.")
}

View file

@ -38,7 +38,7 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.lastOrEmpty
import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.commands.links.LinksMgr.Companion.entries
import net.thauvin.erik.mobibot.commands.links.LinksManager.Companion.entries
import net.thauvin.erik.mobibot.entries.EntriesUtils
import net.thauvin.erik.mobibot.entries.EntryLink
import org.pircbotx.hooks.events.PrivateMessageEvent

View file

@ -81,7 +81,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
* Cleans the messages queue.
*/
private fun clean(): Boolean {
return TellMessagesMgr.clean(messages, maxDays.toLong())
return TellManager.clean(messages, maxDays.toLong())
}
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
@ -89,7 +89,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
if (args.isBlank()) {
helpResponse(channel, args, event)
} else if (args.startsWith(View.VIEW_CMD)) {
if (isChannelOp(channel, event) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) {
if (event.isChannelOp(channel) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) {
viewAll(event)
} else {
viewMessages(event)
@ -120,7 +120,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
} else {
if (messages.removeIf {
it.id == id &&
(it.sender.equals(event.user.nick, true) || isChannelOp(channel, event))
(it.sender.equals(event.user.nick, true) || event.isChannelOp(channel))
}) {
save()
event.sendMessage("The message was deleted from the queue.")
@ -167,7 +167,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
* Saves the messages queue.
*/
private fun save() {
TellMessagesMgr.save(serialObject, messages)
TellManager.save(serialObject, messages)
}
/**
@ -291,7 +291,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
initProperties(MAX_DAYS_PROP, MAX_SIZE_PROP)
// Load the message queue
messages.addAll(TellMessagesMgr.load(serialObject))
messages.addAll(TellManager.load(serialObject))
if (clean()) {
save()
}

View file

@ -1,5 +1,5 @@
/*
* TellMessagesMgr.kt
* TellManager.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@ -41,8 +41,8 @@ import java.time.LocalDateTime
/**
* The Tell Messages Manager.
*/
object TellMessagesMgr {
private val logger: Logger = LoggerFactory.getLogger(TellMessagesMgr::class.java)
object TellManager {
private val logger: Logger = LoggerFactory.getLogger(TellManager::class.java)
/**
* Cleans the messages queue.

View file

@ -45,11 +45,11 @@ class Entries(
var lastPubDate = today()
fun load() {
lastPubDate = FeedsMgr.loadFeed(this)
lastPubDate = FeedsManager.loadFeed(this)
}
fun save() {
lastPubDate = today()
FeedsMgr.saveFeed(this)
FeedsManager.saveFeed(this)
}
}

View file

@ -33,7 +33,7 @@ package net.thauvin.erik.mobibot.entries
import com.rometools.rome.feed.synd.SyndCategory
import com.rometools.rome.feed.synd.SyndCategoryImpl
import net.thauvin.erik.mobibot.commands.links.LinksMgr
import net.thauvin.erik.mobibot.commands.links.LinksManager
import java.io.Serializable
import java.util.Calendar
import java.util.Date
@ -169,7 +169,7 @@ class EntryLink(
* Sets the tags.
*/
fun setTags(tags: String) {
setTags(tags.split(LinksMgr.TAG_MATCH))
setTags(tags.split(LinksManager.TAG_MATCH))
}
/**

View file

@ -1,5 +1,5 @@
/*
* FeedsMgr.kt
* FeedsManager.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@ -55,9 +55,9 @@ import kotlin.io.path.exists
/**
* Manages the RSS feeds.
*/
class FeedsMgr private constructor() {
class FeedsManager private constructor() {
companion object {
private val logger: Logger = LoggerFactory.getLogger(FeedsMgr::class.java)
private val logger: Logger = LoggerFactory.getLogger(FeedsManager::class.java)
// The file containing the current entries.
private const val currentXml = "current.xml"

View file

@ -0,0 +1,150 @@
/*
* Mastodon.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Utils
import net.thauvin.erik.mobibot.Utils.prefixIfMissing
import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.mobibot.social.SocialModule
import org.json.JSONException
import org.json.JSONObject
import org.json.JSONWriter
import java.io.IOException
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
class Mastodon : SocialModule() {
override val name = "Mastodon"
override val handle: String?
get() = properties[HANDLE_PROP]
override val isAutoPost: Boolean
get() = isEnabled && properties[Twitter.AUTO_POST_PROP].toBoolean()
override val isValidProperties: Boolean
get() {
for (s in propertyKeys) {
if (AUTO_POST_PROP != s && HANDLE_PROP != s && properties[s].isNullOrBlank()) {
return false
}
}
return true
}
/**
* Formats the entry for posting.
*/
override fun formatEntry(entry: EntryLink): String {
return "${entry.title} via ${entry.nick} on ${entry.channel}\n\n${entry.link}"
}
/**
* Posts on Mastodon.
*/
@Throws(ModuleException::class)
override fun post(message: String, isDm: Boolean): String {
return toot(
apiKey = properties[ACCESS_TOKEN_PROP],
instance = properties[INSTANCE_PROP],
handle = handle,
message = message,
isDm = isDm
)
}
companion object {
// Property keys
const val ACCESS_TOKEN_PROP = "mastodon-access-token"
const val AUTO_POST_PROP = "mastodon-auto-post"
const val HANDLE_PROP = "mastodon-handle"
const val INSTANCE_PROP = "mastodon-instance"
private const val MASTODON_CMD = "mastodon"
/**
* Toots on Mastodon.
*/
@JvmStatic
@Throws(ModuleException::class)
fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String {
val request = HttpRequest.newBuilder()
.uri(URI.create("https://$instance/api/v1/statuses"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer $apiKey")
.POST(
HttpRequest.BodyPublishers.ofString(
JSONWriter.valueToString(
if (isDm) {
mapOf("status" to "${handle?.prefixIfMissing('@')} $message", "visibility" to "direct")
} else {
mapOf("status" to message)
}
)
)
)
.build()
try {
val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
return try {
val jsonResponse = JSONObject(response.body())
if (isDm) {
jsonResponse.getString("content")
} else {
"Your message was posted to ${jsonResponse.getString("url")}"
}
} catch (e: JSONException) {
throw ModuleException("mastodonPost($message)", "A JSON error has occurred: ${e.message}", e)
}
} else {
throw IOException("Status Code: " + response.statusCode())
}
} catch (e: IOException) {
throw ModuleException("mastodonPost($message)", "An IO error has occurred: ${e.message}", e)
} catch (e: InterruptedException) {
throw ModuleException("mastodonPost($message)", "An error has occurred: ${e.message}", e)
}
}
}
init {
commands.add(MASTODON_CMD)
help.add("To post to Mastodon:")
help.add(Utils.helpFormat("%c $MASTODON_CMD <message>"))
properties[AUTO_POST_PROP] = "false"
initProperties(ACCESS_TOKEN_PROP, HANDLE_PROP, INSTANCE_PROP)
}
}

View file

@ -31,57 +31,27 @@
*/
package net.thauvin.erik.mobibot.modules
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.TwitterTimer
import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.commands.links.LinksMgr
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.mobibot.social.SocialModule
import twitter4j.TwitterException
import java.util.Timer
/**
* The Twitter module.
*/
class Twitter : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(Twitter::class.java)
private val timer = Timer(true)
// Twitter auto-posts.
private val entries: MutableSet<Int> = HashSet()
class Twitter : SocialModule() {
override val name = "Twitter"
/**
* Add an entry to be posted on Twitter.
*/
private fun addEntry(index: Int) {
entries.add(index)
}
fun entriesCount(): Int {
return entries.size
}
private val handle: String?
override val handle: String?
get() = properties[HANDLE_PROP]
private fun hasEntry(index: Int): Boolean {
return entries.contains(index)
}
val isAutoPost: Boolean
get() = isEnabled && properties[AUTOPOST_PROP].toBoolean()
override val isAutoPost: Boolean
get() = isEnabled && properties[AUTO_POST_PROP].toBoolean()
override val isValidProperties: Boolean
get() {
for (s in propertyKeys) {
if (AUTOPOST_PROP != s && HANDLE_PROP != s && properties[s].isNullOrBlank()) {
if (AUTO_POST_PROP != s && HANDLE_PROP != s && properties[s].isNullOrBlank()) {
return false
}
}
@ -89,105 +59,31 @@ class Twitter : ThreadedModule() {
}
/**
* Send a notification to the registered Twitter handle.
* Formats the entry for posting.
*/
fun notification(msg: String) {
if (isEnabled && !handle.isNullOrBlank()) {
runBlocking {
launch {
try {
post(message = msg, isDm = true)
if (logger.isDebugEnabled) logger.debug("Notified @$handle: $msg")
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn("Failed to notify @$handle: $msg", e)
}
}
}
}
override fun formatEntry(entry: EntryLink): String {
return "${entry.title} ${entry.link} via ${entry.nick} on ${entry.channel}"
}
/**
* Posts on Twitter.
*/
@Throws(ModuleException::class)
fun post(handle: String = "${properties[HANDLE_PROP]}", message: String, isDm: Boolean): String {
return twitterPost(
properties[CONSUMER_KEY_PROP],
properties[CONSUMER_SECRET_PROP],
properties[TOKEN_PROP],
properties[TOKEN_SECRET_PROP],
handle,
message,
isDm
override fun post(message: String, isDm: Boolean): String {
return tweet(
consumerKey = properties[CONSUMER_KEY_PROP],
consumerSecret = properties[CONSUMER_SECRET_PROP],
token = properties[TOKEN_PROP],
tokenSecret = properties[TOKEN_SECRET_PROP],
handle = handle,
message = message,
isDm = isDm
)
}
/**
* Post an entry to twitter.
*/
fun postEntry(index: Int) {
if (isAutoPost && hasEntry(index) && LinksMgr.entries.links.size >= index) {
val entry = LinksMgr.entries.links[index]
val msg = "${entry.title} ${entry.link} via ${entry.nick} on ${entry.channel}"
runBlocking {
launch {
try {
if (logger.isDebugEnabled) {
logger.debug("Posting {} to Twitter.", index.toLinkLabel())
}
post(message = msg, isDm = false)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn("Failed to post entry on Twitter.", e)
}
}
}
removeEntry(index)
}
}
fun queueEntry(index: Int) {
if (isAutoPost) {
addEntry(index)
if (logger.isDebugEnabled) {
logger.debug("Scheduling {} for posting on Twitter.", index.toLinkLabel())
}
timer.schedule(TwitterTimer(this, index), Constants.TIMER_DELAY * 60L * 1000L)
}
}
fun removeEntry(index: Int) {
entries.remove(index)
}
/**
* Posts to twitter.
*/
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
try {
event.respond(post(event.user.nick, "$args (by ${event.user.nick} on $channel)", false))
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let {
event.respond(it)
}
}
}
/**
* Post all the entries to Twitter on shutdown.
*/
fun shutdown() {
timer.cancel()
if (isAutoPost) {
for (index in entries) {
postEntry(index)
}
}
}
companion object {
// Property keys
const val AUTOPOST_PROP = "twitter-auto-post"
const val AUTO_POST_PROP = "twitter-auto-post"
const val CONSUMER_KEY_PROP = "twitter-consumerKey"
const val CONSUMER_SECRET_PROP = "twitter-consumerSecret"
const val HANDLE_PROP = "twitter-handle"
@ -198,11 +94,11 @@ class Twitter : ThreadedModule() {
private const val TWITTER_CMD = "twitter"
/**
* Posts on Twitter.
* Tweets on Twitter.
*/
@JvmStatic
@Throws(ModuleException::class)
fun twitterPost(
fun tweet(
consumerKey: String?,
consumerSecret: String?,
token: String?,
@ -236,7 +132,7 @@ class Twitter : ThreadedModule() {
commands.add(TWITTER_CMD)
help.add("To post to Twitter:")
help.add(helpFormat("%c $TWITTER_CMD <message>"))
properties[AUTOPOST_PROP] = "false"
properties[AUTO_POST_PROP] = "false"
initProperties(CONSUMER_KEY_PROP, CONSUMER_SECRET_PROP, HANDLE_PROP, TOKEN_PROP, TOKEN_SECRET_PROP)
}
}

View file

@ -0,0 +1,117 @@
/*
* SocialManager.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.social
import net.thauvin.erik.mobibot.Addons
import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.Timer
/**
* Social Manager.
*/
class SocialManager {
private val entries: MutableSet<Int> = HashSet()
private val logger: Logger = LoggerFactory.getLogger(SocialManager::class.java)
private val modules = ArrayList<SocialModule>()
private val timer = Timer(true)
/**
* Adds social modules.
*/
fun add(addons: Addons, vararg modules: SocialModule) {
modules.forEach {
if (addons.add(it)) {
this.modules.add(it)
}
}
}
/**
* Returns the number of entries.
*/
fun entriesCount(): Int = entries.size
/**
* Sends a social notification (dm, etc.)
*/
fun notification(msg: String) {
modules.forEach {
it.notification(msg)
}
}
/**
* Posts to social media.
*/
fun postEntry(index: Int) {
if (entries.contains(index)) {
modules.forEach {
it.postEntry(index)
}
entries.remove(index)
}
}
/**
* Queues an entry for posting to social media.
*/
fun queueEntry(index: Int) {
if (modules.isNotEmpty()) {
entries.add(index)
if (logger.isDebugEnabled) {
logger.debug("Scheduling {} for posting on social media.", index.toLinkLabel())
}
timer.schedule(SocialTimer(this, index), Constants.TIMER_DELAY * 60L * 1000L)
}
}
/**
* Removes entries from queue.
*/
fun removeEntry(index: Int) {
entries.remove(index)
}
/**
* Posts all entries on shutdown.
*/
fun shutdown() {
timer.cancel()
for (index in entries) {
postEntry(index)
}
}
}

View file

@ -0,0 +1,107 @@
/*
* SocialModule.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.social
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.commands.links.LinksManager
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.mobibot.modules.ModuleException
import net.thauvin.erik.mobibot.modules.ThreadedModule
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
abstract class SocialModule : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(SocialManager::class.java)
abstract val handle: String?
abstract val isAutoPost: Boolean
abstract fun formatEntry(entry: EntryLink): String
/**
* Sends a DM.
*/
fun notification(msg: String) {
if (isEnabled && !handle.isNullOrBlank()) {
runBlocking {
launch {
try {
post(message = msg, isDm = true)
if (logger.isDebugEnabled) logger.debug("Notified $handle on $name: $msg")
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn("Failed to notify $handle on $name: $msg", e)
}
}
}
}
}
abstract fun post(message: String, isDm: Boolean): String
/**
* Post entry to social media.
*/
fun postEntry(index: Int) {
if (isAutoPost && LinksManager.entries.links.size >= index) {
runBlocking {
launch {
try {
if (logger.isDebugEnabled) {
logger.debug("Posting {} to $name.", index.toLinkLabel())
}
post(message = formatEntry(LinksManager.entries.links[index]), isDm = false)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(
"Failed to post entry ${index.toLinkLabel()} on $name.",
e
)
}
}
}
}
}
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
try {
event.respond(post("$args (by ${event.user.nick} on $channel)", false))
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let {
event.respond(it)
}
}
}
}

View file

@ -1,5 +1,5 @@
/*
* TwitterTimer.kt
* SocialTimer.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
@ -30,13 +30,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot
package net.thauvin.erik.mobibot.social
import net.thauvin.erik.mobibot.modules.Twitter
import java.util.TimerTask
class TwitterTimer(private var twitter: Twitter, private var index: Int) : TimerTask() {
class SocialTimer(private var socialManager: SocialManager, private var index: Int) : TimerTask() {
override fun run() {
twitter.postEntry(index)
socialManager.postEntry(index)
}
}

View file

@ -33,6 +33,8 @@ package net.thauvin.erik.mobibot
import org.testng.annotations.BeforeSuite
import java.io.IOException
import java.net.InetAddress
import java.net.UnknownHostException
import java.nio.file.Files
import java.nio.file.Paths
import java.util.Properties
@ -56,6 +58,15 @@ open class LocalProperties {
companion object {
private val localProps = Properties()
fun getHostName(): String {
val ciName = System.getenv("CI_NAME")
return ciName ?: try {
InetAddress.getLocalHost().hostName
} catch (ignore: UnknownHostException) {
"Unknown Host"
}
}
fun getProperty(key: String): String {
return if (localProps.containsKey(key)) {
localProps.getProperty(key)

View file

@ -41,31 +41,34 @@ import assertk.assertions.size
import net.thauvin.erik.mobibot.Constants
import org.testng.annotations.Test
class LinksMgrTest {
private val linksMgr = LinksMgr()
class LinksManagerTest {
private val linksManager = LinksManager()
@Test(groups = ["commands", "links"])
fun fetchTitle() {
assertThat(linksMgr.fetchTitle("https://erik.thauvin.net/"), "fetchTitle(Erik)").contains("Erik's Weblog")
assertThat(linksMgr.fetchTitle("https://www.google.com/foo"), "fetchTitle(Foo)").isEqualTo(Constants.NO_TITLE)
assertThat(linksManager.fetchTitle("https://erik.thauvin.net/"), "fetchTitle(Erik)").contains("Erik's Weblog")
assertThat(
linksManager.fetchTitle("https://www.google.com/foo"),
"fetchTitle(Foo)"
).isEqualTo(Constants.NO_TITLE)
}
@Test(groups = ["commands", "links"])
fun testMatches() {
assertThat(linksMgr.matches("https://www.example.com/"), "matches(url)").isTrue()
assertThat(linksMgr.matches("HTTP://erik.thauvin.net/blog/ Erik's Weblog"), "matches(HTTP)").isTrue()
assertThat(linksManager.matches("https://www.example.com/"), "matches(url)").isTrue()
assertThat(linksManager.matches("HTTP://erik.thauvin.net/blog/ Erik's Weblog"), "matches(HTTP)").isTrue()
}
@Test(groups = ["commands", "links"])
fun matchTagKeywordsTest() {
linksMgr.setProperty(LinksMgr.KEYWORDS_PROP, "key1 key2,key3")
linksManager.setProperty(LinksManager.KEYWORDS_PROP, "key1 key2,key3")
val tags = mutableListOf<String>()
linksMgr.matchTagKeywords("Test title with key2", tags)
linksManager.matchTagKeywords("Test title with key2", tags)
assertThat(tags, "tags").contains("key2")
tags.clear()
linksMgr.matchTagKeywords("Test key3 title with key1", tags)
linksManager.matchTagKeywords("Test key3 title with key1", tags)
assertThat(tags, "tags(key1, key3)").all {
contains("key1")
contains("key3")

View file

@ -45,7 +45,7 @@ class ViewTest {
val view = View()
for (i in 1..10) {
LinksMgr.entries.links.add(
LinksManager.entries.links.add(
EntryLink(
"https://www.example.com/$i",
"Example $i",
@ -98,11 +98,11 @@ class ViewTest {
}
assertThat(view.parseArgs(""), "parseArgs()").all {
prop(Pair<Int, String>::first).isEqualTo(LinksMgr.entries.links.size - View.MAX_ENTRIES)
prop(Pair<Int, String>::first).isEqualTo(LinksManager.entries.links.size - View.MAX_ENTRIES)
prop(Pair<Int, String>::second).isEqualTo("")
}
LinksMgr.entries.links.clear()
LinksManager.entries.links.clear()
assertThat(view.parseArgs("4"), "parseArgs(4)").all {
prop(Pair<Int, String>::first).isEqualTo(0)

View file

@ -60,7 +60,7 @@ class TellMessagesMgrTest {
@BeforeClass
fun saveTest() {
TellMessagesMgr.save(testFile.toAbsolutePath().toString(), testMessages)
TellManager.save(testFile.toAbsolutePath().toString(), testMessages)
assertThat(testFile.fileSize()).isGreaterThan(0)
}
@ -75,14 +75,14 @@ class TellMessagesMgrTest {
queued = LocalDateTime.now().minusDays(maxDays)
})
val size = testMessages.size
assertThat(TellMessagesMgr.clean(testMessages, maxDays + 2), "clean(maxDays=${maxDays + 2})").isFalse()
assertThat(TellMessagesMgr.clean(testMessages, maxDays), "clean(maxDays=$maxDays)").isTrue()
assertThat(TellManager.clean(testMessages, maxDays + 2), "clean(maxDays=${maxDays + 2})").isFalse()
assertThat(TellManager.clean(testMessages, maxDays), "clean(maxDays=$maxDays)").isTrue()
assertThat(testMessages, "testMessages").size().isEqualTo(size - 1)
}
@Test(groups = ["commands", "tell"])
fun loadTest() {
val messages = TellMessagesMgr.load(testFile.toAbsolutePath().toString())
val messages = TellManager.load(testFile.toAbsolutePath().toString())
for (i in messages.indices) {
assertThat(messages).index(i).all {
prop(TellMessage::sender).isEqualTo(testMessages[i].sender)

View file

@ -65,7 +65,7 @@ class FeedMgrTest {
@Test(groups = ["entries"])
fun testFeedMgr() {
// Load the feed
assertThat(FeedsMgr.loadFeed(entries), "loadFeed()").isEqualTo("2021-10-31")
assertThat(FeedsManager.loadFeed(entries), "loadFeed()").isEqualTo("2021-10-31")
assertThat(entries.links, "entries.links").size().isEqualTo(2)
entries.links.forEachIndexed { i, entryLink ->
@ -101,7 +101,7 @@ class FeedMgrTest {
val backlogFile = Paths.get("${entries.logsDir}${today()}.xml")
// Save the feed
FeedsMgr.saveFeed(entries, currentFile.name)
FeedsManager.saveFeed(entries, currentFile.name)
assertThat(currentFile, "currentFile").exists()
assertThat(backlogFile, "backlogFile").exists()
@ -110,7 +110,7 @@ class FeedMgrTest {
// Load the test feed
entries.links.clear()
FeedsMgr.loadFeed(entries, currentFile.name)
FeedsManager.loadFeed(entries, currentFile.name)
entries.links.forEachIndexed { i, entryLink ->
assertThat(entryLink.title, "entryLink.title[${i + 1}]").isEqualTo("Example ${i + 1}")

View file

@ -0,0 +1,56 @@
/*
* MastodonTest.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.isSuccess
import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.Mastodon.Companion.toot
import org.testng.annotations.Test
class MastodonTest : LocalProperties() {
@Test(groups = ["modules"])
@Throws(ModuleException::class)
fun testToot() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertThat {
toot(
getProperty(Mastodon.ACCESS_TOKEN_PROP),
getProperty(Mastodon.INSTANCE_PROP),
getProperty(Mastodon.HANDLE_PROP),
msg,
true
)
}.isSuccess().contains(msg)
}
}

View file

@ -35,31 +35,19 @@ import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isSuccess
import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.Twitter.Companion.twitterPost
import net.thauvin.erik.mobibot.modules.Twitter.Companion.tweet
import org.testng.annotations.Test
import java.net.InetAddress
import java.net.UnknownHostException
/**
* The `TwitterTest` class.
*/
class TwitterTest : LocalProperties() {
private val ci: String
get() {
val ciName = System.getenv("CI_NAME")
return ciName ?: try {
InetAddress.getLocalHost().hostName
} catch (ignore: UnknownHostException) {
"Unknown Host"
}
}
@Test(groups = ["modules"])
@Throws(ModuleException::class)
fun testPostTwitter() {
val msg = "Testing Twitter API from $ci"
fun testTweet() {
val msg = "Testing Twitter API from ${getHostName()}"
assertThat {
twitterPost(
tweet(
getProperty(Twitter.CONSUMER_KEY_PROP),
getProperty(Twitter.CONSUMER_SECRET_PROP),
getProperty(Twitter.TOKEN_PROP),

View file

@ -1,9 +1,9 @@
#Generated by the Semver Plugin for Gradle
#Sun Dec 04 17:34:43 PST 2022
version.buildmeta=799
#Mon Dec 05 21:57:24 PST 2022
version.buildmeta=814
version.major=0
version.minor=8
version.patch=0
version.prerelease=rc
version.project=mobibot
version.semver=0.8.0-rc+799
version.semver=0.8.0-rc+814

View file

@ -81,9 +81,9 @@
<li>Performing Google searches
<div><code>mobibot: google mobitopia on irc</code></div>
</li>
<li>Getting answers from <a href="https://wolframalpha.com/">Wolfram Alpha</a>
<li>Getting answers from <a href="https://wolframalpha.com/">Wolfram Alpha</a> and <a href="https://chat.openai.com/chat">ChatGPT</a>
<div><code>mobibot: wolfram days until christmas</code></div>
<div><code>mobibot: wolfram 1 gallon to liter</code></div>
<div><code>mobibot: chatgpt explain quantum computing in simple terms</code></div>
</li>
<li>Displaying weather information
<div><code>mobibot: weather san francisco</code></div>
@ -116,13 +116,13 @@
<li>Random jokes from <a href="https://v2.jokeapi.dev/">Sv443's JokeAPI</a>
<div><code>mobibot: joke</code></div>
</li>
<li>Rolling dice or Playing war and rock paper scissors
<li>Playing dice, war or rock paper scissors
<div><code>mobibot: dice</code></div>
<div><code>mobibot: war</code></div>
<div><code>mobibot: paper</code></div>
<div><code>mobibot: rock</code></div>
</li>
<li>Posting to <a href="https://twitter.com/mobitopia">Twitter</a></li>
<li>Posting to <a href="https://twitter.com/mobitopia">Twitter</a> and <a href="https://joinmastodon.org/">Mastodon</a></li>
</ul>
<p>Some of the internal features include RSS feed backlogs, rolling logs, debugging toggle and much more.</p>